Jenkins Multi-Branch Pipeline ile Kubernetes Ortamına Otomatik Deployment
Ekibiniz büyüdükçe ve proje sayısı arttıkça, her branch için ayrı Jenkins job’ı oluşturmak gerçekten baş ağrısına dönüşüyor. “Acaba bu branch’ın pipeline’ı var mıydı?” sorusunu sormak zorunda kalmak, hem zaman kaybı hem de insan hatası davet ediyor. İşte tam bu noktada Jenkins Multi-Branch Pipeline devreye giriyor ve hayatı ciddi ölçüde kolaylaştırıyor. Bu yazıda, sıfırdan bir Multi-Branch Pipeline kurarak Kubernetes ortamına otomatik deployment yapacağız. Gerçek bir projede karşılaşabileceğiniz senaryoları da ele alacağız.
Jenkins Multi-Branch Pipeline Nedir?
Tek cümleyle özetlemek gerekirse: kaynak kod deposundaki her branch için otomatik olarak ayrı bir pipeline oluşturan Jenkins özelliği. Yeni bir branch açtığınızda Jenkins bunu algılıyor, Jenkinsfile’ı buluyor ve pipeline’ı çalıştırmaya başlıyor. Branch silindiğinde ise ilgili pipeline da temizleniyor. Bu otomasyon, özellikle feature branch workflow’u kullanan ekiplerde iş yükünü dramatik biçimde azaltıyor.
Kubernetes tarafıyla birleşince tablo daha da güçleniyor. Her branch kendi namespace’ine ya da kendi deployment’ına sahip olabiliyor, main branch production’a gidiyor, develop branch staging’e, feature branch’leri ise geçici test ortamlarına deploy ediliyor.
Ön Gereksinimler
Bu rehberi takip edebilmek için aşağıdakilerin hazır olması gerekiyor:
- Çalışan bir Jenkins kurulumu (2.387+ sürümü önerilir)
- Kubernetes cluster’ı (EKS, GKE, bare-metal fark etmez)
- Docker Hub veya özel container registry
- Git deposu (GitHub, GitLab, Bitbucket)
kubectlvehelmaraçlarına aşinalık
Gerekli Jenkins Plugin’leri
Jenkins’e şu plugin’lerin kurulu olduğundan emin olun:
- Pipeline: Temel pipeline desteği
- Multibranch Pipeline: Multi-branch özelliği için zorunlu
- Kubernetes Plugin: Jenkins agent’larını Kubernetes pod olarak çalıştırır
- Docker Pipeline: Docker image build ve push işlemleri
- Git Plugin: Git entegrasyonu
- Credentials Binding: Hassas bilgileri pipeline’da güvenli kullanmak için
Kubernetes’te Jenkins Agent Yapılandırması
Prodüksiyon ortamında Jenkins master’ı statik agent’larla çalıştırmak yerine, Kubernetes pod’larını dinamik agent olarak kullanmak çok daha mantıklı. Hem kaynak kullanımı optimize oluyor hem de izolasyon sağlanıyor.
Önce Jenkins’in Kubernetes cluster’ına erişmesi için gerekli RBAC ayarlarını yapıyoruz:
# jenkins-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins
namespace: jenkins
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: jenkins-role
rules:
- apiGroups: [""]
resources: ["pods", "pods/exec", "pods/log", "persistentvolumeclaims", "events"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["namespaces", "services", "configmaps", "secrets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: jenkins-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: jenkins-role
subjects:
- kind: ServiceAccount
name: jenkins
namespace: jenkins
kubectl apply -f jenkins-rbac.yaml
Ardından Jenkins arayüzünden Manage Jenkins > Manage Nodes and Clouds > Configure Clouds yolunu izleyerek Kubernetes Cloud ekleyin. Kubernetes URL olarak cluster’ınızın API server adresini, credentials olarak az önce oluşturduğunuz service account token’ını kullanın.
Proje Yapısı
Senaryomuzda basit bir Node.js API servisi kullanacağız. Proje dizin yapısı şu şekilde:
myapp/
├── src/
│ └── index.js
├── Dockerfile
├── Jenkinsfile
├── k8s/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── namespace.yaml
├── helm/
│ └── myapp/
│ ├── Chart.yaml
│ ├── values.yaml
│ └── templates/
└── package.json
Jenkinsfile Yazımı
İşte gerçek dünya senaryosunda kullanabileceğiniz, branch’e göre farklı davranışlar sergileyen bir Jenkinsfile:
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
serviceAccountName: jenkins
containers:
- name: docker
image: docker:24-dind
securityContext:
privileged: true
env:
- name: DOCKER_TLS_CERTDIR
value: ""
- name: kubectl
image: bitnami/kubectl:1.28
command: ["sleep", "infinity"]
- name: helm
image: alpine/helm:3.13.0
command: ["sleep", "infinity"]
- name: node
image: node:20-alpine
command: ["sleep", "infinity"]
'''
}
}
environment {
DOCKER_REGISTRY = "registry.mycompany.com"
APP_NAME = "myapp"
DOCKER_CREDENTIALS = credentials('docker-registry-creds')
KUBECONFIG_CRED = credentials('kubeconfig-prod')
}
stages {
stage('Branch Bilgisi') {
steps {
script {
env.BRANCH_SLUG = env.BRANCH_NAME.replaceAll('[^a-zA-Z0-9]', '-').toLowerCase()
env.IMAGE_TAG = "${env.BRANCH_SLUG}-${env.BUILD_NUMBER}"
if (env.BRANCH_NAME == 'main') {
env.DEPLOY_ENV = 'production'
env.K8S_NAMESPACE = 'myapp-production'
env.REPLICAS = '3'
} else if (env.BRANCH_NAME == 'develop') {
env.DEPLOY_ENV = 'staging'
env.K8S_NAMESPACE = 'myapp-staging'
env.REPLICAS = '2'
} else {
env.DEPLOY_ENV = 'preview'
env.K8S_NAMESPACE = "myapp-preview-${env.BRANCH_SLUG}"
env.REPLICAS = '1'
}
echo "Branch: ${env.BRANCH_NAME}"
echo "Ortam: ${env.DEPLOY_ENV}"
echo "Namespace: ${env.K8S_NAMESPACE}"
echo "Image Tag: ${env.IMAGE_TAG}"
}
}
}
stage('Bağımlılıklar ve Test') {
steps {
container('node') {
sh '''
npm ci
npm run lint
npm run test -- --coverage
'''
}
}
post {
always {
junit 'test-results/**/*.xml'
publishHTML([
allowMissing: false,
reportDir: 'coverage',
reportFiles: 'index.html',
reportName: 'Test Coverage'
])
}
}
}
stage('Docker Build') {
steps {
container('docker') {
sh """
docker login ${env.DOCKER_REGISTRY}
-u ${DOCKER_CREDENTIALS_USR}
-p ${DOCKER_CREDENTIALS_PSW}
docker build
--build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
--build-arg VCS_REF=${env.GIT_COMMIT}
--build-arg VERSION=${env.IMAGE_TAG}
-t ${env.DOCKER_REGISTRY}/${env.APP_NAME}:${env.IMAGE_TAG}
-t ${env.DOCKER_REGISTRY}/${env.APP_NAME}:${env.BRANCH_SLUG}-latest
.
docker push ${env.DOCKER_REGISTRY}/${env.APP_NAME}:${env.IMAGE_TAG}
docker push ${env.DOCKER_REGISTRY}/${env.APP_NAME}:${env.BRANCH_SLUG}-latest
"""
}
}
}
stage('Kubernetes Namespace Hazırlık') {
steps {
container('kubectl') {
withCredentials([file(credentialsId: 'kubeconfig-prod', variable: 'KUBECONFIG')]) {
sh """
kubectl get namespace ${env.K8S_NAMESPACE} ||
kubectl create namespace ${env.K8S_NAMESPACE}
kubectl label namespace ${env.K8S_NAMESPACE}
environment=${env.DEPLOY_ENV}
managed-by=jenkins
app=${env.APP_NAME}
--overwrite
"""
}
}
}
}
stage('Helm Deploy') {
steps {
container('helm') {
withCredentials([file(credentialsId: 'kubeconfig-prod', variable: 'KUBECONFIG')]) {
sh """
helm upgrade --install ${env.APP_NAME} ./helm/myapp
--namespace ${env.K8S_NAMESPACE}
--set image.repository=${env.DOCKER_REGISTRY}/${env.APP_NAME}
--set image.tag=${env.IMAGE_TAG}
--set replicaCount=${env.REPLICAS}
--set environment=${env.DEPLOY_ENV}
--set ingress.host=${env.BRANCH_SLUG}.preview.mycompany.com
--wait
--timeout 5m0s
"""
}
}
}
}
stage('Smoke Test') {
steps {
container('node') {
sh """
sleep 10
APP_URL="https://${env.BRANCH_SLUG}.preview.mycompany.com"
curl -f -s --retry 5 --retry-delay 5 $APP_URL/health || exit 1
echo "Smoke test basarili: $APP_URL"
"""
}
}
}
}
post {
success {
slackSend(
channel: '#deployments',
color: 'good',
message: """
Deployment basarili!
Branch: ${env.BRANCH_NAME}
Ortam: ${env.DEPLOY_ENV}
Image: ${env.DOCKER_REGISTRY}/${env.APP_NAME}:${env.IMAGE_TAG}
URL: https://${env.BRANCH_SLUG}.preview.mycompany.com
"""
)
}
failure {
slackSend(
channel: '#deployments',
color: 'danger',
message: "HATA: ${env.BRANCH_NAME} branch deployment basarisiz! Build: ${env.BUILD_URL}"
)
}
cleanup {
cleanWs()
}
}
}
Helm Chart Yapılandırması
Helm chart’ımızın values.yaml dosyası ortam bazlı özelleştirmeye izin vermeli:
# helm/myapp/values.yaml
replicaCount: 1
image:
repository: registry.mycompany.com/myapp
pullPolicy: IfNotPresent
tag: "latest"
service:
type: ClusterIP
port: 3000
ingress:
enabled: true
className: nginx
host: myapp.preview.mycompany.com
tls: true
certManager: true
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 10
targetCPUUtilizationPercentage: 70
environment: production
healthCheck:
path: /health
initialDelaySeconds: 15
periodSeconds: 10
Preview Ortamları için Otomatik Temizlik
Feature branch’leri silindiğinde Kubernetes namespace’lerinin de temizlenmesi gerekiyor. Bunun için Jenkinsfile’a ayrı bir temizlik mekanizması ekliyoruz. Jenkins’in orphaned item strategy özelliğini de kullanabilirsiniz, ancak namespace temizliği için özel bir yaklaşım daha güvenilir:
// Jenkinsfile'ın sonuna eklenecek cleanup pipeline
// Bu kodu ayrı bir "cleanup" Jenkinsfile olarak da tutabilirsiniz
properties([
pipelineTriggers([
// Branch silindiğinde tetikle
[$class: 'GitHubPushTrigger']
])
])
// Branch silindiğinde namespace'i temizle
if (currentBuild.getBuildCauses().toString().contains('BranchEventCause')) {
stage('Preview Namespace Temizlik') {
if (env.BRANCH_NAME != 'main' && env.BRANCH_NAME != 'develop') {
container('kubectl') {
withCredentials([file(credentialsId: 'kubeconfig-prod', variable: 'KUBECONFIG')]) {
sh """
NAMESPACE="myapp-preview-${env.BRANCH_SLUG}"
if kubectl get namespace $NAMESPACE 2>/dev/null; then
kubectl delete namespace $NAMESPACE --grace-period=30
echo "Namespace temizlendi: $NAMESPACE"
fi
"""
}
}
}
}
}
Alternatif olarak, bir CronJob ile eski preview namespace’lerini periyodik temizleyebilirsiniz:
#!/bin/bash
# cleanup-preview-namespaces.sh
# Bu scripti bir Kubernetes CronJob içinde çalıştırabilirsiniz
CUTOFF_HOURS=48
CURRENT_TIME=$(date +%s)
kubectl get namespaces -l managed-by=jenkins,environment=preview
--no-headers -o custom-columns="NAME:.metadata.name,CREATED:.metadata.creationTimestamp" |
while read -r name created; do
CREATED_TIME=$(date -d "$created" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%SZ" "$created" +%s)
AGE_HOURS=$(( (CURRENT_TIME - CREATED_TIME) / 3600 ))
if [ "$AGE_HOURS" -gt "$CUTOFF_HOURS" ]; then
echo "$name namespace'i $AGE_HOURS saatlik, siliniyor..."
kubectl delete namespace "$name"
fi
done
Multi-Branch Pipeline’ı Jenkins’te Oluşturmak
Jenkins arayüzünde şu adımları izleyin:
- New Item tıklayın
- İsim olarak projenin adını girin
- Multibranch Pipeline seçin
- Branch Sources bölümünde Git veya GitHub ekleyin
- Repository URL’ini ve credentials’ı girin
- Discover branches davranışını ayarlayın (All branches önerilir)
- Build Configuration altında Jenkinsfile yolunu belirtin (varsayılan:
Jenkinsfile) - Scan Multibranch Pipeline Triggers altında webhook veya periyodik tarama ayarlayın
- Kaydedin ve Scan Multibranch Pipeline Now butonuna tıklayın
GitHub webhook için şu URL formatını kullanın:
https://jenkins.mycompany.com/multibranch-webhook-trigger/invoke?token=MY_SECRET_TOKEN
Credentials Yönetimi
Hassas bilgileri asla Jenkinsfile’a doğrudan yazmayın. Jenkins Credentials Store’u doğru kullanmak şart:
# kubectl ile secret oluşturup Jenkins'e aktarma
kubectl config view --raw | base64 | tr -d 'n' > kubeconfig-base64.txt
# Sonra bu değeri Jenkins'te:
# Manage Jenkins > Credentials > System > Global credentials
# Kind: Secret file
# ID: kubeconfig-prod
# File: kubeconfig.yaml
Pipeline içinde credentials kullanımı için standart yöntem:
// Birden fazla credential aynı anda kullanmak için
withCredentials([
usernamePassword(
credentialsId: 'docker-registry-creds',
usernameVariable: 'REGISTRY_USER',
passwordVariable: 'REGISTRY_PASS'
),
file(
credentialsId: 'kubeconfig-prod',
variable: 'KUBECONFIG'
),
string(
credentialsId: 'slack-token',
variable: 'SLACK_TOKEN'
)
]) {
sh """
docker login -u $REGISTRY_USER -p $REGISTRY_PASS registry.mycompany.com
kubectl --kubeconfig=$KUBECONFIG apply -f k8s/
"""
}
Sık Karşılaşılan Sorunlar ve Çözümleri
Docker-in-Docker izin hatası: Kubernetes pod’unda Docker build yaparken permission denied hatası alıyorsanız, pod spec’ine securityContext.privileged: true ekleyin. Ama prodüksiyonda bu yaklaşım güvenlik riski taşıyor. Kaniko veya Buildah gibi daemon-less alternatifleri değerlendirin.
Helm timeout sorunu: --wait flag’i ile birlikte uzun süren deployment’larda timeout alabilirsiniz. --timeout değerini artırın ya da uygulamanızın readiness probe’larını gözden geçirin.
Branch adından kaynaklanan Kubernetes kaynak adı hataları: Kubernetes kaynak adları yalnızca lowercase harf, rakam ve tire kabul ediyor. Branch adını replaceAll('[^a-zA-Z0-9]', '-').toLowerCase() gibi bir regex ile temizlemeyi unutmayın.
Image pull hatası: Private registry kullanıyorsanız namespace’e imagePullSecrets eklemeniz gerekiyor:
kubectl create secret docker-registry regcred
--docker-server=registry.mycompany.com
--docker-username=myuser
--docker-password=mypassword
--namespace=myapp-production
Performans İpuçları
Pipeline hızını artırmak için birkaç pratik öneri:
- Paralel stage’ler kullanın: Test ve lint işlemlerini paralel çalıştırabilirsiniz.
- Docker layer cache’ini akıllıca kullanın:
COPY package*.jsonsatırını uygulama kodundan önce yazarak npm install katmanını cache’leyin. - Workspace temizliğini kontrol edin: Her build’de
cleanWs()çağırmak yerine, sadece gerekli dosyaları temizleyin. - Kubernetes agent pod startup süresini azaltın: Sık kullandığınız image’ları node’lara önceden çekin ya da image pull policy’yi
IfNotPresentolarak ayarlayın.
Sonuç
Jenkins Multi-Branch Pipeline ile Kubernetes deployment kombinasyonu, özellikle birden fazla ekibin aynı repository üzerinde çalıştığı ortamlarda gerçek anlamda hayat kurtarıyor. Her branch’in kendi test ortamına sahip olması, “benim bilgisayarımda çalışıyor” problemini büyük ölçüde ortadan kaldırıyor. Pull request açılır açılmaz otomatik bir preview URL’inin oluşması, QA süreçlerini dramatik biçimde hızlandırıyor.
Bu yazıda ele aldığımız yapıyı kendi projenize uyarlarken aşamalar halinde ilerlemenizi öneririm. Önce tek bir branch için basit bir pipeline kurun, çalıştığına emin olun, sonra multi-branch yapısına geçin. Helm chart’ınızı olgunlaştırın ve ancak ondan sonra preview ortamı otomasyonunu devreye alın. Bir anda her şeyi kurmaya çalışmak, debug sürecini gereksiz yere zorlaştırıyor.
Son olarak şunu söylemek isterim: bu tür CI/CD altyapısına yapılan yatırım, ilk haftada karmaşık gelse de birkaç ay içinde kendini birkaç kez amorti ediyor. Ekibin deployment korkusu azalıyor, release sıklığı artıyor ve gece yarısı acil müdahaleleri belirgin biçimde düşüyor. Bundan daha iyi ROI bulmak zor.
