Jenkins Shared Library ile Tekrar Kullanılabilir Pipeline Yapısı
Birden fazla Jenkins pipeline’ı yönetiyorsanız ve her Jenkinsfile’da aynı kodları tekrar tekrar yazıyorsanız, bir noktada “bunu daha iyi bir şekilde yapmanın yolu olmalı” diye düşünmeye başlarsınız. O yol, Jenkins Shared Library. Bu yazıda Shared Library’nin ne olduğunu, nasıl kurulduğunu ve gerçek dünya senaryolarında nasıl kullanıldığını adım adım inceleyeceğiz.
Shared Library Nedir ve Neden İhtiyaç Duyarsınız?
Diyelim ki 15 farklı mikro servis projeniz var. Her birinin Jenkinsfile’ında Docker image build etme, Slack bildirim gönderme, SonarQube analizi çalıştırma gibi ortak adımlar mevcut. Bu adımları her Jenkinsfile’a kopyalayıp yapıştırdığınızda ne olur? Bir gün Slack webhook URL’si değişir ya da Docker registry adresi güncellenir. 15 Jenkinsfile’ı tek tek güncellemek zorunda kalırsınız.
Shared Library bu sorunu çözer. Ortak Jenkins kodunu merkezi bir Git repository’sinde tutarsınız, tüm pipeline’lar bu kütüphaneyi import eder. Bir değişiklik yaptığınızda bütün pipeline’lar anında güncellenir.
Shared Library’nin sağladığı avantajlar:
- DRY prensibi: Don’t Repeat Yourself. Aynı kodu bir kez yazarsınız.
- Merkezi bakım: Güvenlik açığı veya hata bulduğunuzda tek bir yerde düzeltirsiniz.
- Standartlaşma: Tüm ekipler aynı build, test ve deploy süreçlerini kullanır.
- Test edilebilirlik: Library kodunu bağımsız olarak unit test edebilirsiniz.
- Versiyon kontrolü: Library’yi tag’leyerek pipeline’larda belirli versiyonları kullanabilirsiniz.
Shared Library Proje Yapısı
Jenkins Shared Library’nin beklediği belirli bir dizin yapısı vardır. Bu yapıya uymak zorundasınız, aksi halde Jenkins kodunuzu bulamaz.
jenkins-shared-library/
├── vars/
│ ├── buildDocker.groovy
│ ├── deployToKubernetes.groovy
│ ├── sendSlackNotification.groovy
│ └── runSonarQube.groovy
├── src/
│ └── com/
│ └── sirket/
│ ├── Docker.groovy
│ ├── Kubernetes.groovy
│ └── Utils.groovy
├── resources/
│ └── com/
│ └── sirket/
│ └── docker-compose-template.yml
└── test/
└── groovy/
└── com/
└── sirket/
└── DockerSpec.groovy
Bu yapıdaki dizinlerin görevleri şöyle:
- vars/: Pipeline’lardan direkt çağrılan global fonksiyonlar buraya gider. Her
.groovydosyası bir global değişken veya fonksiyon olarak erişilebilir hale gelir. - src/: Daha karmaşık, nesne yönelimli Groovy sınıfları buraya gider. Standart Java/Groovy paket yapısını izler.
- resources/: Statik dosyalar, template’ler, script’ler buraya konur.
libraryResourcefonksiyonuyla erişilir. - test/: Unit testler için kullanılır, Jenkins tarafından otomatik olarak işlenmez.
Jenkins’te Shared Library Tanımlama
Library’nizi kullanmadan önce Jenkins’e bu repository’yi tanıtmanız gerekiyor. Bunun için iki yöntem var: Global tanımlama ve folder-level tanımlama.
Global Tanımlama
Jenkins ana sayfasından Manage Jenkins > Configure System yolunu izleyin. “Global Pipeline Libraries” bölümünü bulun ve yeni bir kütüphane ekleyin.
Doldurmanız gereken alanlar:
- Name: Library’nizin adı, Jenkinsfile’da bu adı kullanacaksınız. Örneğin
sirket-pipeline-library. - Default version: Varsayılan branch veya tag.
mainya dav1.0.0gibi. - Retrieval method: Modern SCM seçin.
- Source Code Management: Git seçin ve repository URL’sini girin.
- Credentials: Özel repository ise credentials ekleyin.
Jenkinsfile’dan Library Yükleme
Global tanımladıktan sonra Jenkinsfile’ınızın en üstüne şu satırı ekleyin:
@Library('sirket-pipeline-library') _
pipeline {
agent any
stages {
stage('Build') {
steps {
buildDocker(imageName: 'myapp', tag: env.BUILD_NUMBER)
}
}
}
}
Belirli bir versiyon kullanmak isterseniz:
@Library('[email protected]') _
vars/ Dizininde Global Fonksiyon Yazma
Şimdi asıl işe gelelim. vars/ dizinindeki her .groovy dosyası bir global fonksiyon olarak kullanılabilir. En basit yaklaşım call() metodunu tanımlamak.
Docker Build Fonksiyonu
// vars/buildDocker.groovy
def call(Map config = [:]) {
def imageName = config.imageName ?: error("imageName parametresi zorunludur")
def tag = config.tag ?: 'latest'
def registry = config.registry ?: 'registry.sirket.com'
def dockerfile = config.dockerfile ?: 'Dockerfile'
echo "Docker image build ediliyor: ${registry}/${imageName}:${tag}"
sh """
docker build
-f ${dockerfile}
-t ${registry}/${imageName}:${tag}
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
--build-arg GIT_COMMIT=${env.GIT_COMMIT}
.
"""
echo "Docker image registry'ye push ediliyor..."
docker.withRegistry("https://${registry}", 'docker-registry-credentials') {
docker.image("${registry}/${imageName}:${tag}").push()
// Latest tag'i de push edelim
docker.image("${registry}/${imageName}:${tag}").push('latest')
}
return "${registry}/${imageName}:${tag}"
}
Bu fonksiyonu Jenkinsfile’da şöyle kullanırsınız:
@Library('sirket-pipeline-library') _
pipeline {
agent any
stages {
stage('Docker Build & Push') {
steps {
script {
def imageTag = buildDocker(
imageName: 'user-service',
tag: "${env.BRANCH_NAME}-${env.BUILD_NUMBER}",
registry: 'registry.sirket.com'
)
echo "Build tamamlandı: ${imageTag}"
env.IMAGE_TAG = imageTag
}
}
}
}
}
Slack Bildirim Fonksiyonu
Gerçek projede en çok ihtiyaç duyulan şeylerden biri bildirimler. Özellikle gece yarısı production’a deploy yapıldığında veya bir build patladığında ekibi haberdar etmek kritik.
// vars/sendSlackNotification.groovy
def call(Map config = [:]) {
def status = config.status ?: 'INFO'
def message = config.message ?: 'Pipeline durumu güncellendi'
def channel = config.channel ?: '#ci-cd-notifications'
def colorMap = [
'SUCCESS' : '#36a64f',
'FAILURE' : '#ff0000',
'UNSTABLE': '#ffa500',
'INFO' : '#0000ff',
'STARTED' : '#cccccc'
]
def color = colorMap[status] ?: '#cccccc'
def slackMessage = """
*${status}* - ${env.JOB_NAME} #${env.BUILD_NUMBER}
*Branch:* ${env.BRANCH_NAME ?: 'N/A'}
*Mesaj:* ${message}
*Detay:* <${env.BUILD_URL}|Build linkini görüntüle>
*Süre:* ${currentBuild.durationString}
""".stripIndent()
slackSend(
channel: channel,
color: color,
message: slackMessage,
tokenCredentialId: 'slack-bot-token'
)
}
Kubernetes Deploy Fonksiyonu
// vars/deployToKubernetes.groovy
def call(Map config = [:]) {
def namespace = config.namespace ?: 'default'
def deployment = config.deployment ?: error("deployment adı zorunludur")
def image = config.image ?: error("image parametresi zorunludur")
def kubeconfig = config.kubeconfig ?: 'kubeconfig-production'
def waitTimeout = config.waitTimeout ?: '5m'
withCredentials([file(credentialsId: kubeconfig, variable: 'KUBECONFIG')]) {
sh """
kubectl set image deployment/${deployment}
${deployment}=${image}
-n ${namespace}
--record
echo "Rollout bekleniyor..."
kubectl rollout status deployment/${deployment}
-n ${namespace}
--timeout=${waitTimeout}
"""
}
echo "Deploy başarıyla tamamlandı: ${deployment} -> ${image}"
}
src/ Dizininde Sınıf Yazma
Daha karmaşık iş mantığı için src/ dizininde Groovy sınıfları kullanmak daha temiz bir yaklaşım sunar.
// src/com/sirket/Utils.groovy
package com.sirket
class Utils implements Serializable {
private def script
Utils(def script) {
this.script = script
}
def getGitInfo() {
def gitCommit = script.sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
def gitBranch = script.sh(
script: 'git rev-parse --abbrev-ref HEAD',
returnStdout: true
).trim()
def gitAuthor = script.sh(
script: 'git log -1 --format="%an"',
returnStdout: true
).trim()
return [
commit : gitCommit,
branch : gitBranch,
author : gitAuthor
]
}
def generateVersion(String prefix = 'v') {
def timestamp = new Date().format('yyyyMMdd-HHmmss')
def gitInfo = getGitInfo()
return "${prefix}${timestamp}-${gitInfo.commit}"
}
def isMainBranch() {
def branch = script.env.BRANCH_NAME ?: getGitInfo().branch
return branch in ['main', 'master']
}
def isPullRequest() {
return script.env.CHANGE_ID != null
}
}
Bu sınıfı vars/ fonksiyonlarından veya Jenkinsfile’dan şöyle kullanırsınız:
// vars/getVersionInfo.groovy
import com.sirket.Utils
def call() {
def utils = new Utils(this)
def version = utils.generateVersion()
def gitInfo = utils.getGitInfo()
echo "Versiyon: ${version}"
echo "Git commit: ${gitInfo.commit}"
echo "Branch: ${gitInfo.branch}"
echo "Author: ${gitInfo.author}"
return [version: version, gitInfo: gitInfo]
}
Gerçek Dünya Senaryosu: Tam Bir Mikro Servis Pipeline’ı
Şimdi her şeyi bir araya getirelim. Bir e-ticaret şirketinde çalıştığınızı ve “order-service” adlı bir mikro servisi deploy etmeniz gerektiğini düşünün. CI/CD akışı şöyle olmalı: Kod test edilecek, Docker image build edilecek, staging’e otomatik deploy yapılacak, production’a onaylı deploy yapılacak.
// Jenkinsfile (order-service repository'sinde)
@Library('sirket-pipeline-library@main') _
pipeline {
agent {
kubernetes {
label 'jenkins-agent'
defaultContainer 'jnlp'
yaml """
apiVersion: v1
kind: Pod
spec:
containers:
- name: maven
image: maven:3.8.6-openjdk-17
command: ['sleep', '9999999']
- name: docker
image: docker:latest
command: ['sleep', '9999999']
volumeMounts:
- name: docker-sock
mountPath: /var/run/docker.sock
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
"""
}
}
environment {
SERVICE_NAME = 'order-service'
REGISTRY = 'registry.sirket.com'
STAGING_NS = 'staging'
PROD_NS = 'production'
}
stages {
stage('Başlatılıyor') {
steps {
script {
sendSlackNotification(
status: 'STARTED',
message: "Build başlatıldı: ${env.SERVICE_NAME}",
channel: '#backend-team'
)
def versionInfo = getVersionInfo()
env.APP_VERSION = versionInfo.version
env.GIT_AUTHOR = versionInfo.gitInfo.author
}
}
}
stage('Unit Testler') {
steps {
container('maven') {
sh 'mvn test -Dspring.profiles.active=test'
}
}
post {
always {
junit 'target/surefire-reports/*.xml'
jacoco(
execPattern: 'target/jacoco.exec',
minimumLineCoverage: '70'
)
}
}
}
stage('SonarQube Analizi') {
steps {
container('maven') {
runSonarQube(
projectKey: env.SERVICE_NAME,
qualityGateTimeout: 300
)
}
}
}
stage('Docker Build') {
steps {
container('docker') {
script {
env.IMAGE_TAG = buildDocker(
imageName: env.SERVICE_NAME,
tag: env.APP_VERSION,
registry: env.REGISTRY
)
}
}
}
}
stage('Staging Deploy') {
steps {
script {
deployToKubernetes(
deployment: env.SERVICE_NAME,
image: env.IMAGE_TAG,
namespace: env.STAGING_NS,
kubeconfig: 'kubeconfig-staging'
)
}
}
}
stage('Entegrasyon Testleri') {
steps {
sh """
cd integration-tests
./run-tests.sh --env staging --service ${env.SERVICE_NAME}
"""
}
}
stage('Production Deploy Onayı') {
when {
branch 'main'
}
steps {
timeout(time: 30, unit: 'MINUTES') {
input message: "Production'a deploy edilsin mi?",
ok: 'Onayla',
submitter: 'senior-devops,tech-lead'
}
}
}
stage('Production Deploy') {
when {
branch 'main'
}
steps {
script {
deployToKubernetes(
deployment: env.SERVICE_NAME,
image: env.IMAGE_TAG,
namespace: env.PROD_NS,
kubeconfig: 'kubeconfig-production',
waitTimeout: '10m'
)
}
}
}
}
post {
success {
sendSlackNotification(
status: 'SUCCESS',
message: "${env.SERVICE_NAME} v${env.APP_VERSION} başarıyla deploy edildi. Yapan: ${env.GIT_AUTHOR}",
channel: '#backend-team'
)
}
failure {
sendSlackNotification(
status: 'FAILURE',
message: "${env.SERVICE_NAME} pipeline başarısız oldu! Acil kontrol edin.",
channel: '#backend-team'
)
}
unstable {
sendSlackNotification(
status: 'UNSTABLE',
message: "${env.SERVICE_NAME} pipeline unstable durumda, test sonuçlarını kontrol edin.",
channel: '#backend-team'
)
}
}
}
Bu Jenkinsfile ne kadar temiz görünüyor değil mi? Tüm implementasyon detayları Shared Library’de, burada sadece iş akışı var.
SonarQube Fonksiyonu Örneği
// vars/runSonarQube.groovy
def call(Map config = [:]) {
def projectKey = config.projectKey ?: error("projectKey zorunludur")
def sonarUrl = config.sonarUrl ?: 'http://sonarqube.sirket.com'
def qualityGateTimeout = config.qualityGateTimeout ?: 300
withSonarQubeEnv('SonarQube-Server') {
sh """
mvn sonar:sonar
-Dsonar.projectKey=${projectKey}
-Dsonar.host.url=${sonarUrl}
-Dsonar.java.coveragePlugin=jacoco
-Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
"""
}
timeout(time: qualityGateTimeout, unit: 'SECONDS') {
def qualityGate = waitForQualityGate()
if (qualityGate.status != 'OK') {
error "SonarQube Quality Gate başarısız: ${qualityGate.status}"
}
}
echo "SonarQube analizi tamamlandı ve Quality Gate geçildi."
}
Library’yi Dinamik Olarak Yükleme
Bazen @Library annotation yerine script içinde dinamik yükleme yapmak isteyebilirsiniz. Bu, koşullu library yükleme senaryolarında işe yarar.
// Jenkinsfile içinde dinamik yükleme
pipeline {
agent any
stages {
stage('Setup') {
steps {
script {
// Dinamik olarak library yükle
def lib = library(
identifier: 'sirket-pipeline-library@main',
retriever: modernSCM([
$class: 'GitSCMSource',
remote: 'https://github.com/sirket/jenkins-shared-library.git',
credentialsId: 'github-token'
])
)
// src/ içindeki sınıfı kullan
def utils = lib.com.sirket.Utils.new(this)
echo "Versiyon: ${utils.generateVersion()}"
}
}
}
}
}
Library Geliştirirken Dikkat Edilmesi Gerekenler
Serializable interface’i uygulayın: src/ içindeki sınıflar Serializable implement etmeli. Jenkins pipeline’lar checkpoint’lenebilir olmalı ve bu, sınıfların serileştirilebilir olmasını gerektirir.
CPS (Continuation Passing Style) farkındalığı: vars/ içindeki Groovy kodu Jenkins tarafından CPS dönüşümüne tabi tutulur. Karmaşık closure’lar ve bazı Groovy idiom’ları beklenmedik şekilde davranabilir. Sorun yaşarsanız @NonCPS annotation’ını kullanın.
@NonCPS kullanımı: Serileştirilebilir olmayan nesnelerle çalışan metodları @NonCPS ile işaretleyin.
// vars/parseJson.groovy
import groovy.json.JsonSlurperClassic
@NonCPS
def call(String jsonText) {
def parser = new JsonSlurperClassic()
return parser.parseText(jsonText)
}
Error handling: Library fonksiyonlarınızda anlamlı hata mesajları verin. error() fonksiyonu build’i durdurup log’a mesaj yazar.
Idempotency: Özellikle deploy fonksiyonlarının birden fazla çalıştırılabilir olmasına özen gösterin.
Library’yi Test Etme
Shared Library’yi production’a almadan önce test etmek kritik. Jenkins Pipeline Unit Test Framework kullanabilirsiniz.
# Proje kök dizininde Maven ile test çalıştırma
mvn test
# Veya Gradle ile
./gradlew test
pom.xml veya build.gradle dosyanıza JenkinsPipelineUnit bağımlılığını eklemeniz gerekiyor. Test dosyaları test/ dizininde tutun ve her kritik fonksiyon için test yazın. Bu, library’nin beklenmedik şekillerde davranmasını engeller ve refactoring yaparken güven verir.
Versiyonlama Stratejisi
Shared Library’nizin versiyonlamasını ciddiye alın. Önerim şu strateji:
- main/master branch: Geliştirme ortamı, en güncel kod.
- v1.x, v2.x gibi semver branch’leri: Kararlı versiyonlar.
- Git tag’leri: Kesin release noktaları, örneğin
v1.2.3.
Production Jenkinsfile’larınızda her zaman belirli bir tag kullanın:
@Library('[email protected]') _
Böylece library’de breaking change yapıldığında mevcut pipeline’lar etkilenmez. Hazır olduğunuzda versiyonu manuel güncelersiniz.
Sonuç
Jenkins Shared Library, büyüyen bir CI/CD altyapısının olmazsa olmaz parçasıdır. Başta “ekstra iş gibi” görünebilir, ancak 5-10 pipeline’dan sonra sağladığı kolaylık tartışmasız hale gelir.
Özetlersek neler kazanırsınız: Tüm pipeline’lardaki ortak kod tek bir yerde toplanır, bir değişiklik yaptığınızda anında tüm projelere yansır, ekip içinde standartlar otomatik olarak uygulanır, yeni bir proje başladığında birkaç satır Jenkinsfile ile tam CI/CD hattını ayağa kaldırabilirsiniz.
Başlangıç için küçük adım atın: Sadece Slack bildirimlerini veya Docker build adımını library’ye taşıyın. Bunun faydasını gördükçe giderek daha fazla kodu ortak kütüphaneye alırsınız. Sonunda Jenkinsfile’larınız birer konfigürasyon dosyası kadar sade ve okunabilir olur, implementasyon detaylarından arınmış, iş mantığını anlatan temiz bir yapıya kavuşursunuz.
