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 .groovy dosyası 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. libraryResource fonksiyonuyla 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. main ya da v1.0.0 gibi.
  • 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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir