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ı:
@NonCPSannotation’ı veyascript {}bloğu kullanın - Workspace Kirliği:
cleanWs()ile workspace’i temizleyin, özellikle Docker agent kullanmıyorsanız - Büyük Artifact’lar:
archiveArtifactsile gereksiz dosyaları arşivlemeyin, disk dolabilir - Credential’ların Log’a Düşmesi:
set +xkullanı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 noneile 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.
