Jenkins Pipeline Türleri: Declarative ve Scripted Pipeline Karşılaştırması
Jenkins’i ilk kurduğunuzda karşınıza çıkan en büyük soru şu oluyor: “Pipeline’ımı nasıl yazacağım?” İki farklı yaklaşım var ve ikisi de aynı işi yapıyor ama çok farklı felsefelere sahip. Declarative ve Scripted pipeline arasındaki farkı anlamak, Jenkins’le uzun vadeli çalışacaksanız kaçınılmaz. Bu yazıda her iki yaklaşımı da gerçek dünya örnekleriyle inceleyeceğiz, hangisini ne zaman kullanmanız gerektiğini netleştireceğiz.
Pipeline Nedir, Neden Önemli?
Jenkins’in eski dünyasında her şeyi GUI üzerinden yapılandırırdınız. Build adımlarını tıklayarak ekler, post-build action’ları checkbox’larla seçerdiniz. Bu yaklaşımın büyük problemi şuydu: Pipeline tanımınız Jenkins’in içinde gizli kalıyordu, versiyon kontrolüne alamıyordunuz ve bir şeyin neden çalıştığını anlamak için Jenkins’e girip saatlerce bakmanız gerekiyordu.
Pipeline as Code kavramı tam burada devreye giriyor. Jenkinsfile adında bir dosya oluşturuyorsunuz, onu Git reponuza atıyorsunuz ve artık pipeline’ınız kod gibi davranıyor. Değişiklikleri commit edebiliyorsunuz, review yapabiliyorsunuz, geri alabiliyorsunuz.
Jenkins’te iki farklı pipeline syntax’ı var:
- Declarative Pipeline: Yapısal, kural bazlı, Jenkins’in önerdiği modern yaklaşım
- Scripted Pipeline: Groovy tabanlı, esnek, eski ve güçlü yaklaşım
Declarative Pipeline
Declarative pipeline, Jenkins 2.x ile birlikte gelen ve okunabilirliği ön plana çıkaran yaklaşım. Belirli bir yapıya uymak zorundasınız ama bu zorunluluk aslında bir avantaja dönüşüyor, herkes dosyaya baktığında ne olduğunu anlıyor.
Temel Yapı
pipeline {
agent any
environment {
APP_NAME = 'myapp'
DOCKER_REGISTRY = 'registry.mycompany.com'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
}
}
stage('Test') {
steps {
sh 'mvn test'
}
post {
always {
junit 'target/surefire-reports/**/*.xml'
}
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sh 'kubectl apply -f k8s/'
}
}
}
post {
success {
slackSend channel: '#deployments', message: "Build başarılı: ${env.JOB_NAME}"
}
failure {
slackSend channel: '#deployments', message: "Build başarısız: ${env.JOB_NAME}"
}
}
}
Bu yapıya bakınca her bloğun ne işe yaradığını anlamak için Jenkins bilgisi gerekmez bile. agent, stages, post blokları belirli kurallara göre çalışıyor.
Agent Direktifi
Agent, pipeline’ın nerede çalışacağını belirliyor. Birkaç farklı kullanım şekli var:
- agent any: Jenkins’teki herhangi bir node’da çalıştır
- agent none: Her stage için ayrı agent tanımla
- agent { label ‘linux’ }: Belirli etiketle işaretlenmiş node’da çalıştır
- agent { docker ‘maven:3.8’ }: Docker container içinde çalıştır
pipeline {
agent none
stages {
stage('Build on Linux') {
agent { label 'linux && docker' }
steps {
sh 'docker build -t myapp:${BUILD_NUMBER} .'
}
}
stage('Test on Windows') {
agent { label 'windows' }
steps {
bat 'run-tests.bat'
}
}
}
}
Bu örnek gerçek hayatta çok karşılaşılan bir senaryo. Uygulamanızı Linux’ta build edip Windows’ta test etmeniz gerekebilir. Declarative pipeline bunu temiz bir şekilde ifade etmenizi sağlıyor.
Environment ve Credentials
Ortam değişkenleri ve credential yönetimi Declarative pipeline’ın güçlü olduğu alanlardan biri.
pipeline {
agent any
environment {
// Düz değer
BUILD_ENV = 'production'
// Jenkins credentials store'dan çek
DB_PASSWORD = credentials('prod-db-password')
// SSH key pair - otomatik olarak _USR ve _PSW suffix'leri oluşturur
DOCKER_HUB = credentials('docker-hub-credentials')
}
stages {
stage('Deploy') {
steps {
sh '''
echo "Docker Hub kullanıcısı: $DOCKER_HUB_USR"
docker login -u $DOCKER_HUB_USR -p $DOCKER_HUB_PSW
docker push ${DOCKER_HUB_USR}/myapp:${BUILD_NUMBER}
'''
}
}
}
}
credentials() helper’ı otomatik olarak credential’ı maskeler, log’larda şifre görünmez. Bu küçük ama kritik bir güvenlik detayı.
When Direktifi ile Koşullu Çalışma
pipeline {
agent any
stages {
stage('Unit Tests') {
steps {
sh 'pytest tests/unit/'
}
}
stage('Integration Tests') {
when {
anyOf {
branch 'main'
branch 'develop'
changeRequest()
}
}
steps {
sh 'pytest tests/integration/'
}
}
stage('Production Deploy') {
when {
allOf {
branch 'main'
not { changeRequest() }
environment name: 'DEPLOY_ENABLED', value: 'true'
}
}
steps {
sh './deploy.sh production'
}
}
}
}
when bloğu içinde anyOf, allOf, not kombinasyonlarıyla oldukça karmaşık koşullar yazabiliyorsunuz. Feature branch’lerden sadece unit test çalıştırıp main’e gelince full deploy yapmak istediğinizde bu çok işe yarıyor.
Parallel Stage’ler
Build sürelerini kısaltmanın en etkili yolu parallel çalıştırma. Declarative bunu şık bir şekilde destekliyor:
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'gradle build'
}
}
stage('Test Suite') {
parallel {
stage('Unit Tests') {
steps {
sh 'gradle test --tests "com.myapp.unit.*"'
}
}
stage('Integration Tests') {
steps {
sh 'gradle test --tests "com.myapp.integration.*"'
}
}
stage('Security Scan') {
agent { label 'security-scanner' }
steps {
sh 'trivy image myapp:latest'
}
}
}
}
stage('Deploy to Staging') {
steps {
sh './deploy.sh staging'
}
}
}
}
Üç paralel aşama aynı anda çalışacak. Build süresi en uzun olan kadar sürecek. Production ortamında 45 dakika süren test süitini bu yöntemle 15 dakikaya indirdiğimizi bizzat yaşadım.
Scripted Pipeline
Scripted pipeline, Groovy’nin tüm gücünü kullanmanıza izin veriyor. Declarative gibi katı bir yapısı yok, daha özgür ama aynı zamanda daha riskli. Her şey node bloğu içinde başlıyor.
Temel Yapı
node('linux') {
def appVersion = ''
def dockerImage = ''
try {
stage('Checkout') {
checkout scm
appVersion = sh(
script: 'git describe --tags --always',
returnStdout: true
).trim()
echo "Build version: ${appVersion}"
}
stage('Build') {
sh "docker build -t myapp:${appVersion} ."
dockerImage = "myapp:${appVersion}"
}
stage('Test') {
sh "docker run --rm ${dockerImage} pytest"
}
stage('Push') {
withCredentials([usernamePassword(
credentialsId: 'docker-hub',
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS'
)]) {
sh "docker login -u ${DOCKER_USER} -p ${DOCKER_PASS}"
sh "docker push ${dockerImage}"
}
}
} catch (Exception e) {
currentBuild.result = 'FAILURE'
throw e
} finally {
stage('Cleanup') {
sh "docker rmi ${dockerImage} || true"
cleanWs()
}
}
}
Burada Groovy’nin try-catch-finally yapısını doğrudan kullanabiliyorsunuz. Variable tanımlama, string interpolation, her şey standart Groovy kodu.
Scripted Pipeline’ın Gerçek Gücü: Dinamik Stage Oluşturma
Declarative pipeline’ın yapamadığı şeylerden biri, çalışma zamanında stage sayısını dinamik olarak belirlemek. Scripted bunu mümkün kılıyor:
def deployEnvironments = ['dev', 'staging', 'production']
def parallelStages = [:]
node {
stage('Build') {
sh 'mvn package'
}
deployEnvironments.each { env ->
parallelStages["Deploy to ${env}"] = {
node("${env}-agent") {
stage("Deploy ${env}") {
sh "kubectl --context=${env} apply -f k8s/"
sh "./smoke-test.sh ${env}"
}
}
}
}
stage('Parallel Deploy') {
parallel parallelStages
}
}
Bu örnek gerçekten güçlü. Environment listesini dışarıdan okuyabilirsiniz, bir config dosyasından, bir API’den veya başka bir kaynaktan. Stage’ler dinamik oluşturuluyor.
Shared Library Kullanımı
Hem Declarative hem de Scripted pipeline’da shared library kullanabilirsiniz ama Scripted’da bu çok daha doğal hissettiriyor. Jenkins shared library yapısı:
// vars/deployApp.groovy - Shared library fonksiyonu
def call(Map config) {
def appName = config.appName ?: 'unknown'
def environment = config.environment ?: 'dev'
def imageTag = config.imageTag ?: 'latest'
echo "Deploying ${appName}:${imageTag} to ${environment}"
sh """
helm upgrade --install ${appName} ./charts/${appName}
--namespace ${environment}
--set image.tag=${imageTag}
--set environment=${environment}
--wait --timeout 5m
"""
// Health check
sh "./scripts/health-check.sh ${appName} ${environment}"
}
Scripted pipeline’da bu shared library fonksiyonunu çağırmak:
@Library('my-shared-library') _
node {
def imageTag = ''
stage('Build') {
imageTag = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
sh "docker build -t myapp:${imageTag} ."
sh "docker push registry.mycompany.com/myapp:${imageTag}"
}
stage('Deploy Dev') {
deployApp(
appName: 'myapp',
environment: 'dev',
imageTag: imageTag
)
}
stage('Deploy Staging') {
input message: 'Staging'e deploy edilsin mi?', ok: 'Onayla'
deployApp(
appName: 'myapp',
environment: 'staging',
imageTag: imageTag
)
}
}
Declarative vs Scripted: Hangisi Ne Zaman?
İki yaklaşım arasında seçim yaparken dikkat etmeniz gereken faktörler şunlar:
Declarative tercih edin, eğer:
- Ekibinizde Jenkins’e yeni başlayan biri varsa, Declarative’i okumak çok daha kolay
- Standart build/test/deploy pipeline’ı kuruyorsanız ve fazla özelleştirmeye ihtiyaç yoksa
- Jenkins Blue Ocean plugin kullanıyorsanız, görsel pipeline editörü sadece Declarative destekliyor
- IDE desteği ve linting istiyorsanız, Declarative için araçlar daha olgunlaşmış durumda
Scripted tercih edin, eğer:
- Pipeline logic’iniz karmaşık ve conditional dallanmalar içeriyorsa
- Çalışma zamanında dinamik stage oluşturmanız gerekiyorsa
- Groovy ile kompleks veri manipülasyonu yapmanız gerekiyorsa
- Mevcut Scripted pipeline’larınız var ve migration maliyeti yüksekse
Pratikte gördüğüm en iyi yaklaşım şu: Yeni projeler için Declarative ile başlayın, bir noktada Declarative’in yetersiz kaldığını hissederseniz o spesifik bölüm için script {} bloğu kullanın.
Declarative İçinde Script Bloğu
Declarative’in kısıtlamalarını aşmanın en temiz yolu script {} bloğu. Bu sayede Declarative’in yapısal avantajlarını korurken Groovy’nin gücünden faydalanabiliyorsunuz:
pipeline {
agent any
stages {
stage('Prepare') {
steps {
script {
// Burada Groovy kodu yazabilirsiniz
def gitTag = sh(
script: 'git describe --tags --exact-match 2>/dev/null || echo ""',
returnStdout: true
).trim()
if (gitTag) {
env.DEPLOY_VERSION = gitTag
env.IS_RELEASE = 'true'
} else {
env.DEPLOY_VERSION = "${env.BUILD_NUMBER}-snapshot"
env.IS_RELEASE = 'false'
}
echo "Version: ${env.DEPLOY_VERSION}, Release: ${env.IS_RELEASE}"
}
}
}
stage('Build & Push') {
steps {
sh "docker build -t myapp:${env.DEPLOY_VERSION} ."
sh "docker push registry.mycompany.com/myapp:${env.DEPLOY_VERSION}"
}
}
stage('Release Deploy') {
when {
environment name: 'IS_RELEASE', value: 'true'
}
steps {
sh "helm upgrade myapp ./charts --set image.tag=${env.DEPLOY_VERSION}"
}
}
}
}
Bu yaklaşım ikisinin ortasında mükemmel bir denge sağlıyor.
Gerçek Dünya Senaryosu: Microservice CI/CD Pipeline
Bir e-ticaret sisteminin order-service’i için gerçekçi bir pipeline yazalım. Bu senaryo şu gereksinimleri karşılamalı: branch’e göre farklı davranış, Docker build ve push, Kubernetes deploy, Slack bildirimi, rollback mekanizması.
pipeline {
agent any
environment {
SERVICE_NAME = 'order-service'
REGISTRY = 'registry.ecommerce.com'
K8S_NAMESPACE = "${env.BRANCH_NAME == 'main' ? 'production' : 'staging'}"
SLACK_CHANNEL = '#platform-alerts'
}
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: 30, unit: 'MINUTES')
disableConcurrentBuilds()
}
stages {
stage('Checkout & Version') {
steps {
checkout scm
script {
env.GIT_COMMIT_SHORT = sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
env.IMAGE_TAG = "${env.BUILD_NUMBER}-${env.GIT_COMMIT_SHORT}"
env.FULL_IMAGE = "${REGISTRY}/${SERVICE_NAME}:${env.IMAGE_TAG}"
}
}
}
stage('Build & Test') {
parallel {
stage('Unit Tests') {
steps {
sh 'go test ./... -coverprofile=coverage.out'
sh 'go tool cover -html=coverage.out -o coverage.html'
}
post {
always {
publishHTML([
reportName: 'Coverage Report',
reportDir: '.',
reportFiles: 'coverage.html'
])
}
}
}
stage('Docker Build') {
steps {
sh "docker build -t ${env.FULL_IMAGE} --build-arg VERSION=${env.IMAGE_TAG} ."
}
}
}
}
stage('Security Scan') {
steps {
sh "trivy image --exit-code 1 --severity HIGH,CRITICAL ${env.FULL_IMAGE}"
}
}
stage('Push Image') {
steps {
withCredentials([usernamePassword(
credentialsId: 'registry-credentials',
usernameVariable: 'REG_USER',
passwordVariable: 'REG_PASS'
)]) {
sh "docker login ${REGISTRY} -u ${REG_USER} -p ${REG_PASS}"
sh "docker push ${env.FULL_IMAGE}"
}
}
}
stage('Deploy') {
steps {
script {
def previousRevision = sh(
script: "helm history ${SERVICE_NAME} -n ${K8S_NAMESPACE} --max 1 --output json | jq -r '.[0].revision' 2>/dev/null || echo '0'",
returnStdout: true
).trim()
env.PREVIOUS_REVISION = previousRevision
}
sh """
helm upgrade --install ${SERVICE_NAME} ./helm/${SERVICE_NAME}
--namespace ${K8S_NAMESPACE}
--set image.repository=${REGISTRY}/${SERVICE_NAME}
--set image.tag=${env.IMAGE_TAG}
--wait --timeout 10m
"""
}
}
stage('Smoke Test') {
steps {
sh "./scripts/smoke-test.sh ${K8S_NAMESPACE}"
}
post {
failure {
script {
if (env.PREVIOUS_REVISION != '0') {
echo "Smoke test başarısız, rollback yapılıyor..."
sh "helm rollback ${SERVICE_NAME} ${env.PREVIOUS_REVISION} -n ${K8S_NAMESPACE}"
}
}
}
}
}
}
post {
success {
slackSend(
channel: "${SLACK_CHANNEL}",
color: 'good',
message: ":white_check_mark: ${SERVICE_NAME} başarıyla deploy edildi.nVersiyon: ${env.IMAGE_TAG}nOrtam: ${K8S_NAMESPACE}"
)
}
failure {
slackSend(
channel: "${SLACK_CHANNEL}",
color: 'danger',
message: ":x: ${SERVICE_NAME} deploy başarısız!nBuild: ${env.BUILD_URL}nOrtam: ${K8S_NAMESPACE}"
)
}
always {
sh "docker rmi ${env.FULL_IMAGE} || true"
cleanWs()
}
}
}
Bu pipeline production ortamında kullandığımız gerçek bir şablona çok yakın. Paralel build ve test, güvenlik taraması, Helm deploy, otomatik rollback ve Slack bildirimi hepsi bir arada.
Pipeline Yazarken Dikkat Edilmesi Gerekenler
Deneyimlerimden öğrendiğim birkaç kritik nokta:
- Workspace temizliği: Her build sonunda
cleanWs()çağırın, disk dolar ve sonra saatlerce neden build başlamıyor diye bakarsınız - Timeout ayarı:
options { timeout(time: 30, unit: 'MINUTES') }mutlaka ekleyin, takılı kalan bir build tüm executorları tıkayabilir - Concurrent build: Aynı branch’te iki deploy aynı anda başlarsa ortam bozulabilir,
disableConcurrentBuilds()bunu engeller - Credential maskeleme: Şifreleri
echoile yazdırmayın, Jenkins maskeler ama alışkanlık olarak çevrenize kötü örnek olursunuz - Error handling: Scripted pipeline’da try-catch kullanmayı unutmayın, Declarative’de
post { failure {} }bloğu bu görevi görüyor
Sonuç
Declarative ve Scripted pipeline arasındaki seçim aslında felsefi bir tercih. Declarative size kurallar koyuyor, sizi belirli bir yapıya hapsediyor ama bu hapishane çoğu zaman sizi koruma altına da alıyor. Yeni başlayanlar hata yapma şansını azaltıyor, okunabilirlik artıyor, takım içi tutarlılık sağlanıyor.
Scripted ise özgürlük veriyor ama bu özgürlükle birlikte sorumluluk da geliyor. Karmaşık iş akışları, dinamik yapılar ve Groovy’nin tam gücü gereken durumlarda Scripted’ın değerini anlıyorsunuz.
Pratikte en iyi yaklaşım şu: Yeni pipeline’larınızı Declarative ile yazın. Bir noktada script {} bloğuna ihtiyaç duyduğunuzda onu kullanın. Eğer bir pipeline’ın yarısından fazlası script {} bloğu dolmaya başladıysa, o zaman tamamen Scripted’a geçmeyi değerlendirin. Bu pragmatik yaklaşım hem okunabilirliği hem de esnekliği korumanızı sağlar.
Jenkins’in dokümantasyonunda da belirtildiği gibi resmi öneri Declarative. Yeni özellikler önce Declarative’e geliyor, Blue Ocean desteği var, IDE entegrasyonları daha iyi. Eğer sıfırdan başlıyorsanız Declarative ile başlayın, zaten er geç Scripted’a da ihtiyaç duyacaksınız ama temel ihtiyaçlarınızın büyük çoğunluğunu Declarative karşılayacak.
