Jenkinsfile Yazımı: Pipeline as Code Rehberi

Jenkinsfile yazmaya başladığınızda ilk hissettiğiniz şey genellikle şudur: “Bu da ne böyle?” Onlarca parametre, Groovy syntax’ı, agent tanımları, stage’ler… İnsan bir süre sonra her şeyi UI üzerinden tıklayarak yapmak istiyor. Ama sabır gösterip Pipeline as Code yaklaşımını bir kez içselleştirdiğinizde, geri dönmek istemiyorsunuz. Çünkü pipeline’ınız artık versiyon kontrolünde, ekip arkadaşlarınız PR üzerinden inceleyebiliyor ve “ya jenkinde ne değiştirdin?” sorusu ortadan kalkıyor.

Bu yazıda gerçek dünya senaryoları üzerinden Jenkinsfile yazmayı, Declarative Pipeline ile Scripted Pipeline arasındaki farkları, paralel stage’leri, credential yönetimini ve production ortamına deploy pipeline’ı oluşturmayı ele alacağız.

Pipeline as Code Neden Önemli?

Klasik Jenkins job’larında ne yapıyordunuz? Ekrana giriyordunuz, shell script yazıyordunuz, build trigger ayarlıyordunuz ve bir şekilde çalışıyordu. Sonra Jenkins sunucunuz patladı ve o job’ların nasıl yapılandırıldığını hatırlamak için ekip arkadaşınızı aradınız. O da hatırlamıyordu.

Pipeline as Code yaklaşımı bu problemi temelden çözüyor:

  • Versiyonlama: Her pipeline değişikliği Git geçmişinde görünür
  • Code Review: Pipeline değişiklikleri PR sürecinden geçer
  • Reproducibility: Aynı Jenkinsfile’ı farklı ortamlarda çalıştırabilirsiniz
  • Disaster Recovery: Jenkins’i sıfırdan kurup Jenkinsfile’ı çektiğinizde her şey çalışır
  • Ekip Şeffaflığı: Developer’lar pipeline’ın nasıl çalıştığını görebilir, katkı yapabilir

Declarative vs Scripted Pipeline

Jenkins’te iki farklı pipeline yazım stili var. Yeni başlıyorsanız Declarative Pipeline kullanın. Daha okunabilir, hata mesajları daha anlamlı ve Jenkins’in sağladığı built-in özellikleri daha kolay kullanıyorsunuz.

Declarative Pipeline yapısı şöyle görünür:

pipeline {
    agent any

    environment {
        APP_NAME = 'myapp'
        DEPLOY_ENV = 'staging'
    }

    stages {
        stage('Build') {
            steps {
                echo "Building ${APP_NAME}..."
                sh 'mvn clean package -DskipTests'
            }
        }

        stage('Test') {
            steps {
                sh 'mvn test'
            }
            post {
                always {
                    junit 'target/surefire-reports/*.xml'
                }
            }
        }

        stage('Deploy') {
            steps {
                echo "Deploying to ${DEPLOY_ENV}"
                sh './scripts/deploy.sh'
            }
        }
    }

    post {
        success {
            slackSend message: "Build basarili: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
        }
        failure {
            slackSend color: 'danger', message: "Build basarisiz: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
        }
    }
}

Scripted Pipeline ise Groovy’nin tüm gücünü kullanmanıza izin verir ama daha fazla boilerplate kod yazmanızı gerektirir. Çok karmaşık mantıklar için tercih edilir:

node {
    try {
        stage('Checkout') {
            checkout scm
        }

        stage('Build') {
            def mvnHome = tool 'Maven-3.8'
            env.PATH = "${mvnHome}/bin:${env.PATH}"
            sh 'mvn clean package'
        }

        stage('Test') {
            sh 'mvn test'
        }

    } catch (Exception e) {
        currentBuild.result = 'FAILURE'
        throw e
    } finally {
        cleanWs()
    }
}

Çoğu durumda Declarative Pipeline yeterli. Gerçekten dinamik agent seçimi veya runtime’da stage ekleme gibi ihtiyaçlarınız varsa Scripted’a geçebilirsiniz.

Agent Tanımları

Agent, pipeline’ın nerede çalışacağını belirler. Bu kısım özellikle Kubernetes veya Docker tabanlı ortamlarda kritik hale geliyor.

pipeline {
    // Herhangi bir agent uzerinde calistir
    agent any

    stages {
        stage('Docker ile Build') {
            agent {
                docker {
                    image 'maven:3.8-openjdk-11'
                    args '-v /root/.m2:/root/.m2'
                }
            }
            steps {
                sh 'mvn clean package'
            }
        }

        stage('Node ile Test') {
            agent {
                docker {
                    image 'node:18-alpine'
                }
            }
            steps {
                sh 'npm install && npm test'
            }
        }

        stage('Kubernetes Agent') {
            agent {
                kubernetes {
                    yaml '''
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: maven
    image: maven:3.8-openjdk-11
    command:
    - sleep
    args:
    - infinity
'''
                    defaultContainer 'maven'
                }
            }
            steps {
                sh 'mvn --version'
            }
        }
    }
}

Belirli bir node label’ı üzerinde çalıştırmak için:

pipeline {
    agent {
        label 'linux && docker'
    }

    stages {
        stage('Build') {
            steps {
                sh 'docker build -t myapp:latest .'
            }
        }
    }
}

Environment Variables ve Credentials Yönetimi

Production Jenkinsfile’larında en çok dikkat edilmesi gereken konu credential yönetimidir. Şifreyi, API key’i, token’ı Jenkinsfile’a direkt yazmak en büyük güvenlik hatasıdır.

Jenkins’in credentials() binding’ini kullanın:

pipeline {
    agent any

    environment {
        // Bu sekilde global environment variable tanimlayabilirsiniz
        APP_VERSION = sh(returnStdout: true, script: 'git describe --tags --always').trim()
        DOCKER_REGISTRY = 'registry.sirketim.com'

        // Credential binding - Jenkins Credentials Store'dan cekiyor
        DOCKER_CREDENTIALS = credentials('docker-registry-credentials')
        AWS_ACCESS_KEY_ID = credentials('aws-access-key-id')
        SONAR_TOKEN = credentials('sonarqube-token')
    }

    stages {
        stage('Docker Login') {
            steps {
                // DOCKER_CREDENTIALS_USR ve DOCKER_CREDENTIALS_PSW otomatik olusur
                sh '''
                    echo $DOCKER_CREDENTIALS_PSW | docker login $DOCKER_REGISTRY 
                        -u $DOCKER_CREDENTIALS_USR 
                        --password-stdin
                '''
            }
        }

        stage('SSH ile Deploy') {
            steps {
                // SSH key credential kullanimi
                sshagent(['production-ssh-key']) {
                    sh '''
                        ssh -o StrictHostKeyChecking=no [email protected] 
                            "cd /app && docker-compose pull && docker-compose up -d"
                    '''
                }
            }
        }

        stage('Secret Text Kullanimi') {
            steps {
                withCredentials([string(credentialsId: 'api-secret-key', variable: 'API_KEY')]) {
                    sh '''
                        curl -H "Authorization: Bearer $API_KEY" 
                             https://api.sirketim.com/deploy
                    '''
                }
            }
        }
    }
}

Önemli not: Jenkins, log’larda credential değerlerini otomatik olarak ** ile maskeler. Ama shell script içinde set -x kullanmaktan kaçının, bu credential’ları açık hale getirebilir.

Paralel Stage’ler

Build süresini kısaltmanın en etkili yolu bağımsız işleri paralel çalıştırmak. Test ve statik analizi aynı anda çalıştırmak, Docker image’ları farklı platformlar için aynı anda build etmek gibi senaryolarda paralel stage’ler hayat kurtarır.

pipeline {
    agent any

    stages {
        stage('Build') {
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }

        stage('Parallel Quality Checks') {
            parallel {
                stage('Unit Tests') {
                    steps {
                        sh 'mvn test -Dtest=UnitTest*'
                    }
                    post {
                        always {
                            junit 'target/surefire-reports/TEST-*UnitTest*.xml'
                        }
                    }
                }

                stage('Integration Tests') {
                    steps {
                        sh 'mvn test -Dtest=IntegrationTest*'
                    }
                    post {
                        always {
                            junit 'target/surefire-reports/TEST-*IntegrationTest*.xml'
                        }
                    }
                }

                stage('SonarQube Analysis') {
                    steps {
                        withSonarQubeEnv('SonarQube') {
                            sh 'mvn sonar:sonar'
                        }
                    }
                }

                stage('OWASP Dependency Check') {
                    steps {
                        sh 'mvn org.owasp:dependency-check-maven:check'
                    }
                }
            }
        }

        stage('Quality Gate') {
            steps {
                timeout(time: 5, unit: 'MINUTES') {
                    waitForQualityGate abortPipeline: true
                }
            }
        }
    }
}

Paralel stage’lerde bir stage başarısız olduğunda diğerlerinin ne yapacağını failFast ile kontrol edebilirsiniz:

stage('Parallel Tests') {
    failFast true
    parallel {
        stage('Test Suite A') {
            steps {
                sh './run-tests.sh suite-a'
            }
        }
        stage('Test Suite B') {
            steps {
                sh './run-tests.sh suite-b'
            }
        }
    }
}

failFast true ayarıyla bir suite başarısız olduğunda diğeri de hemen durur. Bu özellikle uzun süren test suite’lerinde zaman kazandırır.

When Direktifi ile Koşullu Stage’ler

Her commit için her şeyi çalıştırmak kaynak israfı. when direktifi ile hangi branch’te, hangi koşulda hangi stage’in çalışacağını kontrol edebilirsiniz:

pipeline {
    agent any

    environment {
        IS_MAIN_BRANCH = "${env.BRANCH_NAME == 'main'}"
    }

    stages {
        stage('Build') {
            steps {
                sh 'mvn clean package'
            }
        }

        stage('Deploy to Staging') {
            when {
                anyOf {
                    branch 'develop'
                    branch 'staging'
                }
            }
            steps {
                sh './deploy.sh staging'
            }
        }

        stage('Deploy to Production') {
            when {
                allOf {
                    branch 'main'
                    not { changeRequest() }
                    environment name: 'DEPLOY_ENABLED', value: 'true'
                }
            }
            steps {
                input message: 'Production ortamina deploy edilsin mi?',
                      ok: 'Evet, deploy et',
                      submitter: 'admin,lead-dev'
                sh './deploy.sh production'
            }
        }

        stage('Tag Release') {
            when {
                expression {
                    return env.BRANCH_NAME == 'main' && currentBuild.result == null
                }
            }
            steps {
                sh "git tag -a v${APP_VERSION} -m 'Release ${APP_VERSION}'"
                sh 'git push --tags'
            }
        }
    }
}

input adımı production deploy için çok kritik. Pipeline bekler, yetkili kişi onaylar, sonra devam eder. Timeout eklemek de iyi pratik:

stage('Production Onay') {
    when {
        branch 'main'
    }
    steps {
        timeout(time: 2, unit: 'HOURS') {
            input message: 'Production deploy onayliyor musunuz?',
                  ok: 'Onayliyorum',
                  submitter: 'devops-team,cto'
        }
    }
}

Gerçek Dünya Senaryosu: Mikroservis Pipeline

Diyelim ki bir e-ticaret platformu geliştiriyorsunuz. Her mikroservis için benzer ama özelleştirilebilir pipeline lazım. Shared library kullanmak yerine şimdilik tek bir Jenkinsfile üzerinden gidelim:

pipeline {
    agent none

    options {
        buildDiscarder(logRotator(numToKeepStr: '10'))
        timeout(time: 45, unit: 'MINUTES')
        disableConcurrentBuilds()
        timestamps()
    }

    environment {
        SERVICE_NAME = 'order-service'
        DOCKER_REGISTRY = 'registry.eticaret.com'
        IMAGE_TAG = "${env.BRANCH_NAME}-${env.BUILD_NUMBER}"
        KUBECONFIG = credentials('k8s-kubeconfig')
    }

    stages {
        stage('Checkout ve Bilgi Toplama') {
            agent { label 'linux' }
            steps {
                checkout scm
                script {
                    env.GIT_COMMIT_MSG = sh(
                        returnStdout: true,
                        script: 'git log -1 --pretty=%B'
                    ).trim()
                    env.GIT_AUTHOR = sh(
                        returnStdout: true,
                        script: 'git log -1 --pretty=%an'
                    ).trim()
                }
                echo "Commit: ${env.GIT_COMMIT_MSG} - Author: ${env.GIT_AUTHOR}"
            }
        }

        stage('Build ve Test') {
            agent {
                docker {
                    image 'maven:3.8-openjdk-17'
                    args '-v maven-cache:/root/.m2'
                }
            }
            steps {
                sh 'mvn clean verify'
            }
            post {
                always {
                    junit 'target/surefire-reports/*.xml'
                    jacoco(
                        execPattern: 'target/jacoco.exec',
                        classPattern: 'target/classes',
                        sourcePattern: 'src/main/java'
                    )
                }
            }
        }

        stage('Docker Build ve Push') {
            agent { label 'docker' }
            when {
                anyOf { branch 'main'; branch 'develop'; branch pattern: 'release/*', comparator: 'GLOB' }
            }
            steps {
                withCredentials([usernamePassword(
                    credentialsId: 'docker-registry-cred',
                    usernameVariable: 'REGISTRY_USER',
                    passwordVariable: 'REGISTRY_PASS'
                )]) {
                    sh '''
                        echo $REGISTRY_PASS | docker login $DOCKER_REGISTRY -u $REGISTRY_USER --password-stdin
                        docker build -t $DOCKER_REGISTRY/$SERVICE_NAME:$IMAGE_TAG .
                        docker build -t $DOCKER_REGISTRY/$SERVICE_NAME:latest .
                        docker push $DOCKER_REGISTRY/$SERVICE_NAME:$IMAGE_TAG
                        docker push $DOCKER_REGISTRY/$SERVICE_NAME:latest
                        docker logout $DOCKER_REGISTRY
                    '''
                }
            }
        }

        stage('Deploy Staging') {
            agent { label 'linux' }
            when { branch 'develop' }
            steps {
                sh '''
                    kubectl --kubeconfig=$KUBECONFIG set image deployment/$SERVICE_NAME 
                        $SERVICE_NAME=$DOCKER_REGISTRY/$SERVICE_NAME:$IMAGE_TAG 
                        -n staging
                    kubectl --kubeconfig=$KUBECONFIG rollout status deployment/$SERVICE_NAME 
                        -n staging 
                        --timeout=5m
                '''
            }
        }

        stage('Smoke Test') {
            agent { label 'linux' }
            when { branch 'develop' }
            steps {
                sh '''
                    sleep 10
                    curl -f https://staging.eticaret.com/order-service/health || exit 1
                    echo "Smoke test basarili"
                '''
            }
        }

        stage('Deploy Production') {
            agent { label 'linux' }
            when { branch 'main' }
            steps {
                timeout(time: 1, unit: 'HOURS') {
                    input message: "${SERVICE_NAME} production'a deploy edilsin mi?nImage: ${IMAGE_TAG}",
                          ok: 'Deploy Et',
                          submitter: 'devops,team-lead'
                }
                sh '''
                    kubectl --kubeconfig=$KUBECONFIG set image deployment/$SERVICE_NAME 
                        $SERVICE_NAME=$DOCKER_REGISTRY/$SERVICE_NAME:$IMAGE_TAG 
                        -n production
                    kubectl --kubeconfig=$KUBECONFIG rollout status deployment/$SERVICE_NAME 
                        -n production 
                        --timeout=10m
                '''
            }
        }
    }

    post {
        success {
            slackSend(
                channel: '#deployments',
                color: 'good',
                message: """
                    Basarili: ${SERVICE_NAME} deploy edildi!
                    Branch: ${env.BRANCH_NAME}
                    Build: #${env.BUILD_NUMBER}
                    Image: ${env.IMAGE_TAG}
                    Author: ${env.GIT_AUTHOR}
                """.stripIndent()
            )
        }
        failure {
            slackSend(
                channel: '#deployments',
                color: 'danger',
                message: """
                    HATA: ${SERVICE_NAME} deploy basarisiz!
                    Branch: ${env.BRANCH_NAME}
                    Build: #${env.BUILD_NUMBER}
                    Log: ${env.BUILD_URL}console
                """.stripIndent()
            )
        }
        always {
            cleanWs()
        }
    }
}

Options ve Önemli Direktifler

Pipeline davranışını kontrol eden options bloğu çok işlevsel:

  • buildDiscarder: Eski build log’larını otomatik siler, disk dolmasını önler
  • timeout: Pipeline takılırsa otomatik keser
  • disableConcurrentBuilds: Aynı anda iki build çalışmasını engeller
  • timestamps: Her log satırına timestamp ekler
  • retry: Başarısız step’leri otomatik tekrar çalıştırır
  • skipDefaultCheckout: Otomatik checkout’u devre dışı bırakır
options {
    buildDiscarder(logRotator(numToKeepStr: '20', daysToKeepStr: '30'))
    timeout(time: 1, unit: 'HOURS')
    disableConcurrentBuilds(abortPrevious: true)
    timestamps()
    retry(2)
    ansiColor('xterm')
}

abortPrevious: true özellikle çok sayıda commit atıldığında önceki build’ları iptal ederek kaynak tasarrufu sağlar. Feature branch geliştirmede çok kullanışlı.

Shared Libraries ile DRY Yaklaşımı

Birden fazla mikroservisiniz varsa aynı pipeline kodunu her yerde tekrarlamak zorunda kalırsınız. Jenkins Shared Libraries bunu çözer. vars/ dizinine koyduğunuz Groovy dosyaları, tüm pipeline’lardan çağrılabilir global fonksiyon haline gelir.

Shared library içinde vars/dockerBuildAndPush.groovy:

def call(Map config) {
    def registry = config.registry ?: 'registry.sirketim.com'
    def serviceName = config.serviceName
    def imageTag = config.imageTag ?: env.BUILD_NUMBER

    withCredentials([usernamePassword(
        credentialsId: 'docker-registry-cred',
        usernameVariable: 'REGISTRY_USER',
        passwordVariable: 'REGISTRY_PASS'
    )]) {
        sh """
            echo $REGISTRY_PASS | docker login ${registry} -u $REGISTRY_USER --password-stdin
            docker build -t ${registry}/${serviceName}:${imageTag} .
            docker push ${registry}/${serviceName}:${imageTag}
            docker logout ${registry}
        """
    }

    echo "Image basariyla push edildi: ${registry}/${serviceName}:${imageTag}"
}

Sonra Jenkinsfile’da kullanımı:

@Library('sirketim-shared-lib') _

pipeline {
    agent any

    stages {
        stage('Docker Push') {
            steps {
                dockerBuildAndPush(
                    registry: 'registry.sirketim.com',
                    serviceName: 'order-service',
                    imageTag: "${env.BRANCH_NAME}-${env.BUILD_NUMBER}"
                )
            }
        }
    }
}

Bu yaklaşımla pipeline kodunuzu merkezi bir repository’de yönetir, tüm projelerde tutarlı bir CI/CD deneyimi sağlarsınız.

Yaygın Hatalar ve Çözümleri

Jenkinsfile yazarken en sık karşılaşılan sorunlar:

  • Groovy Sandbox Kısıtlamaları: @NonCPS annotation’ı veya script {} bloğu kullanın
  • Workspace Kirliği: cleanWs() ile workspace’i temizleyin, özellikle Docker agent kullanmıyorsanız
  • Büyük Artifact’lar: archiveArtifacts ile gereksiz dosyaları arşivlemeyin, disk dolabilir
  • Credential’ların Log’a Düşmesi: set +x kullanın shell script başında
  • Timeout Ayarlamamak: Network gecikmesi veya deadlock durumlarında pipeline sonsuza kadar bekleyebilir
  • Agent None Kullanmadan Global Tool Tanımı: agent none ile pipeline açılıyorsa her stage kendi agent’ını belirtmeli

Sonuç

Jenkinsfile yazmak başta karmaşık görünse de doğru yapılandırıldığında CI/CD süreçlerinizi inanılmaz şeffaf ve yönetilebilir hale getiriyor. Declarative Pipeline ile başlayın, basit build/test/deploy pipeline’ı kurun, sonra adım adım paralel stage’ler, koşullu deploy’lar ve shared library’ler gibi gelişmiş özelliklere geçin.

En önemli nokta: Pipeline’ınızı da kod gibi davranın. Code review, linting (Jenkins’in built-in Replay özelliği veya jenkins-pipeline-linter), versiyon kontrolü ve dökümantasyon. Bu alışkanlıkları edindikten sonra “Jenkins’e girip bir şeyler tıklama” dönemine asla geri dönmek istemeyeceksiniz.

Bir sonraki adım olarak Jenkins Configuration as Code (JCasC) konusuna bakmanızı tavsiye ederim. Jenkins’in kendisini de kod olarak yönetmek, gerçek anlamda sıfırdan ayağa kalkabilir bir CI/CD altyapısı kurmanızı sağlıyor.

Bir yanıt yazın

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