Jenkins'te Parallel, Matrix, input ve Deploy Stage'leri
Aynı paketi önce staging'e, agent'sız bir input onayından sonra production'a sshagent, rsync ve systemctl ile deploy ediyoruz; Lockable Resources eşzamanlılığı, milestone ise sırayı çözer. Health check'li deploy script'i, eski build'in yeniden sonra deploy ettiği gerçek kilit çıktısı, inversePrecedence, failFast, Node 22/24 matrix'i ve IIS için app_offline.htm yapısı.
Sürüm notu: Bu yazıdaki örnekler Jenkins 2.580.2 LTS (Long-Term Support), Lockable Resources plugin'i 1582, Pipeline: Milestone Step, SSH (Secure Shell) Agent plugin'i, systemd çalıştıran Debian 13 container'ları ve .NET 10 ile denenmiştir. IIS (Internet Information Services) deploy'u bu makinede çalıştırılmadı.
Önce Tahmin Et
Staging'e deploy eden bir pipeline'ın iki build'i neredeyse aynı anda başlıyor: önce commit a1b2c3d için #4, iki saniye sonra daha yeni commit e4f5a6b için #5. Deploy adımı lock('bookstore-staging') ile korunuyor, yani aynı anda yalnız biri deploy edebiliyor. İkisi de bittiğinde staging'de hangi commit çalışıyor olur? Kilit "sırayla" deploy etmeyi garanti ediyor ama hangi sırayla?
Bookstore'un Deploy Akışı
Bir önceki yazıda pipeline tek bir bookstore-1.4.N.tgz paketi üretmişti. Şimdi o paketi önce staging'e, bir insanın onayından sonra production'a götürüyoruz. Jenkinsfile'ın Package'dan sonraki kısmı:
stage('Deploy to staging') {
when { branch 'main' }
agent { label 'docker' }
steps {
lock('bookstore-staging') {
unstash 'package'
sshagent(credentials: ['deploy-ssh-key']) {
sh 'ci/deploy.sh jenkwork-staging "$VERSION" "bookstore-$VERSION.tgz"'
}
}
}
}
stage('Approve production') {
when { branch 'main' }
options {
timeout(time: 1, unit: 'HOURS')
}
input {
id 'ApproveProduction'
message 'Deploy this version to production?'
ok 'Deploy'
submitter 'release-manager'
}
steps {
milestone(ordinal: 10, label: 'production approved')
}
}
stage('Deploy to production') {
when { branch 'main' }
agent { label 'docker' }
steps {
lock('bookstore-production') {
unstash 'package'
sshagent(credentials: ['deploy-ssh-key']) {
sh 'ci/deploy.sh jenkwork-prod "$VERSION" "bookstore-$VERSION.tgz"'
}
}
}
}
Package stage'i paketi arşivlemenin yanında stash name: 'package' ile de saklıyor; deploy stage'leri aynı dosyayı unstash ediyor. Yani test edilen paket ile staging'e ve production'a giden paket aynı byte'lar; deploy sırasında hiçbir şey yeniden build edilmiyor. when { branch 'main' } üç stage'i de yalnız main'e sınırlıyor: PR (Pull Request) build'leri hiçbir zaman deploy credential'ına dokunmuyor.
input ve agent none: Onayı Beklerken Executor Tutmamak
"Approve production" stage'inde agent yok. Bu ayrıntı önemli: bir build'in onay için saatlerce, belki bir hafta sonuna kadar beklediğini düşün. Stage bir agent'ta olsaydı, o bekleme süresince bir executor boşuna kilitli kalırdı. Pipeline'ın üst seviyesinde agent none olduğu ve bu stage kendi agent'ını almadığı için bekleme yalnızca controller'da hafif bir kayıt. Declarative'deki input direktifi ayrıca stage'in agent'ından önce değerlendirilir; agent'lı bir stage'de bile onay alınmadan executor alınmaz.
submitter 'release-manager' onaylayabilecek kullanıcıyı (ya da grubu) sınırlıyor; options { timeout(...) } ise bir saat içinde onay gelmezse build'i abort ediyor ve bekleyen build'ler sonsuza kadar birikmiyor. Build onay noktasına geldiğinde log:
[2026-11-08T04:42:45.765Z] Lock released on resource [Resource: bookstore-staging], resources: [bookstore-staging]
[2026-11-08T04:42:45.816Z] Timeout set to expire in 1 hr 0 min
[2026-11-08T04:42:45.830Z] Deploy this version to production?
[2026-11-08T04:42:45.830Z] Deploy or Abort
Onayı arayüzdeki butonla veriyorsun; ben release-manager kullanıcısıyla input'un REST (Representational State Transfer) endpoint'ini çağırdım: POST /job/bookstore/job/main/5/input/ApproveProduction/proceedEmpty. id vermek bu URL'yi (Uniform Resource Locator) öngörülebilir yapıyor; vermezsen Jenkins mesajdan bir id üretir. Onaydan sonra:
[2026-11-08T04:43:03.115Z] Approved by release-manager
[2026-11-08T04:43:03.132Z] Trying to pass milestone 10
[2026-11-08T04:43:03.170Z] Running on docker-agent-1 in /home/jenkins/agent/workspace/bookstore_main
[2026-11-08T04:43:03.231Z] Trying to acquire lock on [Resource: bookstore-production]
[2026-11-08T04:43:03.232Z] Lock acquired on [Resource: bookstore-production], resources: [bookstore-production]
[2026-11-08T04:43:03.522Z] + ci/deploy.sh jenkwork-prod 1.4.5 bookstore-1.4.5.tgz
[2026-11-08T04:43:03.522Z] Warning: Permanently added 'jenkwork-prod' (ED25519) to the list of known hosts.
[2026-11-08T04:43:03.522Z] curl: (7) Failed to connect to localhost port 5000 after 0 ms: Could not connect to server
[2026-11-08T04:43:04.873Z] {"status":"Healthy"}
[2026-11-08T04:43:04.889Z] Lock released on resource [Resource: bookstore-production], resources: [bookstore-production]
Finished: SUCCESS
"Approved by release-manager" satırı build'in kalıcı kaydında kalıyor: kim, ne zaman onayladı sorusunun cevabı burada. Onaydan sonra stage tekrar bir agent alıyor ve workspace'e aynı commit'i checkout ediyor.
Linux'a Deploy: sshagent, rsync ve systemctl
Deploy mantığı Jenkinsfile'da değil, repository'deki küçük bir script'te. Bu hem okunabilirliği artırıyor hem de script'i Jenkins olmadan da denemeyi mümkün kılıyor:
#!/usr/bin/env bash
# kullanım: ci/deploy.sh <host> <version> <package.tgz>
set -euo pipefail
HOST="$1"; VERSION="$2"; PACKAGE="$3"
SSH_OPTS="-o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=$WORKSPACE/.known_hosts"
RELEASE="/opt/bookstore/releases/$VERSION"
rsync -az -e "ssh $SSH_OPTS" "$PACKAGE" "deploy@$HOST:/tmp/$PACKAGE"
ssh $SSH_OPTS "deploy@$HOST" bash -s -- "$RELEASE" "/tmp/$PACKAGE" <<'REMOTE'
set -euo pipefail
mkdir -p "$1"
tar -xzf "$2" -C "$1"
ln -sfn "$1" /opt/bookstore/current
rm -f "$2"
sudo systemctl restart bookstore-api
for i in $(seq 1 20); do
if curl -fsS http://localhost:5000/health; then echo; exit 0; fi
sleep 1
done
echo "health check failed" >&2
exit 1
REMOTE
Her sürüm /opt/bookstore/releases/<version> altına açılıyor ve current symlink'i yeni klasöre çevriliyor. Geri dönmek, symlink'i bir önceki sürüme çevirip servisi yeniden başlatmak kadar kolay. Sunucu tarafında deploy kullanıcısının sudo yetkisi yalnız iki komutla sınırlı: systemctl restart bookstore-api ve systemctl is-active bookstore-api. Uygulama ise ayrı bir bookstore kullanıcısıyla, şu systemd unit'iyle çalışıyor:
[Unit]
Description=Bookstore API
After=network.target
[Service]
User=bookstore
WorkingDirectory=/opt/bookstore/current
ExecStart=/usr/share/dotnet/dotnet /opt/bookstore/current/Bookstore.Api.dll
Environment=ASPNETCORE_HTTP_PORTS=5000
Environment=DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Bu yazıdaki staging ve production sunucuları systemd çalıştıran iki Debian 13 container'ı; gerçek bir VM'de (Virtual Machine) akış aynı. Log'daki curl: (7) Failed to connect satırı health check döngüsünün ilk denemesi: servis yeniden başlarken port bir an kapalı, bir saniye sonra {"status":"Healthy"} geliyor. Health check olmasaydı "deploy başarılı" demek, yalnız "dosyalar kopyalandı" demek olurdu. Deploy sonrası staging sunucusunun kendisi:
$ ls -l /opt/bookstore/
lrwxrwxrwx 1 deploy deploy 29 Nov 8 04:42 current -> /opt/bookstore/releases/1.4.5
drwxr-xr-x 1 deploy deploy 4096 Nov 8 04:42 releases
$ curl -s localhost:5000/api/books/1
{"id":1,"title":"Domain-Driven Design","author":"Eric Evans","price":54.90,"stock":12}
$ curl -s localhost:5000/ | grep -o "<title>[^<]*</title>"
<title>BookstoreWeb</title>
$ systemctl is-active bookstore-api
active
API (Application Programming Interface) ve Angular uygulaması aynı paketten, aynı process'ten servis ediliyor. systemd unit'inin ayrıntılarını (restart politikaları, journald, güvenlik ayarları) ilerideki Web Server'lar ve Hosting serisinin "Linux'ta .NET Uygulamasını systemd Service Olarak Çalıştırmak" yazısında konuşacağız.
Lockable Resources ve milestone
Baştaki sorunun cevabı. Önce yalnız lock kullanan küçük bir job'u iki farklı commit parametresiyle art arda tetikledim.
Build #4 (commit a1b2c3d):
Trying to acquire lock on [Resource: bookstore-staging]
The resource [bookstore-staging] is locked by build lock-demo #5 #5 since Nov 8, 2026, 4:47 AM.
[Resource: bookstore-staging] is not free, waiting for execution ...
Lock acquired on [Resource: bookstore-staging], resources: [bookstore-staging]
Build 4 deploys a1b2c3d to staging
Build #5 (commit e4f5a6b):
Lock acquired on [Resource: bookstore-staging], resources: [bookstore-staging]
Build 5 deploys e4f5a6b to staging
#4 daha önce tetiklenmişti ama kilidi #5 önce aldı; #4 bekledi ve #5'ten sonra deploy etti. Sonuç: staging'de eski commit a1b2c3d çalışıyor. Kilit eşzamanlılığı çözüyor, sırayı çözmüyor. Sıranın aracı milestone: bir build bir milestone'u geçtiğinde, o milestone'u henüz geçmemiş daha eski build'ler iptal edilir. Kilitle birlikte inversePrecedence: true (bekleyenlerden en yenisi kilidi önce alır) kullanınca:
lock(resource: 'bookstore-staging', inversePrecedence: true) {
milestone(ordinal: 1, label: 'deploy staging')
echo "Build ${BUILD_NUMBER} deploys ${params.COMMIT} to staging"
sleep time: 10, unit: 'SECONDS'
}
Build #2 (commit 1111111):
Trying to acquire lock on [Resource: bookstore-staging]
The resource [bookstore-staging] is locked by build lock-milestone #3 #3 since Nov 8, 2026, 4:48 AM.
[Resource: bookstore-staging] is not free, waiting for execution ...
Superseded by lock-milestone#3
Finished: NOT_BUILT
Build #3 (commit 2222222):
Lock acquired on [Resource: bookstore-staging], resources: [bookstore-staging]
Trying to pass milestone 1
Build 3 deploys 2222222 to staging
Build #4 (commit 3333333):
[Resource: bookstore-staging] is not free, waiting for execution ...
Lock acquired on [Resource: bookstore-staging], resources: [bookstore-staging]
Trying to pass milestone 1
Build 4 deploys 3333333 to staging
En eski build (#2, commit 1111111) NOT_BUILT olarak iptal edildi, en yeni build en son deploy etti. Bookstore'un Jenkinsfile'ında production onayından hemen sonra milestone(ordinal: 10) bunu yapıyor: yeni bir sürüm onaylanıp geçtiğinde, hâlâ onay bekleyen eski build'ler production'a gidemez. Bir ayrıntı: lock'ta adı geçen kaynak tanımlı değilse plugin onu geçici olarak oluşturuyor (log'daki "did not exist. Created."); kalıcı kaynakları ve etiketlerini JCasC (Jenkins Configuration as Code) ile tanımlamak daha düzenli.
parallel, failFast ve matrix
Deploy'dan önceki kontrolleri paralel koşturmak süreyi kısaltır. failFast true, dallardan biri kırıldığı anda diğerlerini durdurur:
stage('Checks') {
failFast true
parallel {
stage('API tests') {
agent { label 'docker' }
steps {
sh 'echo "a test failed"; exit 1'
}
}
stage('Web tests') {
agent { label 'docker' }
steps {
sh 'sleep 60'
}
}
}
}
+ echo a test failed
a test failed
+ exit 1
+ sleep 60
Failed in branch API tests
Sending interrupt signal to process
Terminated
Failed in branch Web tests
ERROR: script returned exit code 1
Finished: FAILURE
60 saniyelik dal birinci dal kırılır kırılmaz sonlandırıldı. Bütün pipeline için aynı davranışı options { parallelsAlwaysFailFast() } verir.
matrix ise aynı stage'leri bir eksenin değerleri için çoğaltır. Angular 22 hem Node 22'yi hem 24'ü desteklediğine göre web testlerini ikisinde de koşturmak anlamlı:
stage('Web on Node versions') {
matrix {
axes {
axis {
name 'NODE_VERSION'
values '22', '24'
}
}
agent {
docker {
image "node:${NODE_VERSION}-trixie"
label 'docker'
args '-v bookstore-npm:/npm'
}
}
environment {
npm_config_cache = '/npm'
}
stages {
stage('Test') {
steps {
git url: 'http://jenkwork-gitea:3000/bookstore/bookstore.git', branch: 'main', credentialsId: 'gitea-ci'
dir('bookstore-web') {
sh 'node --version'
sh 'npm ci --no-audit --no-fund --loglevel=error'
sh 'npx ng test --watch=false'
}
}
}
}
}
}
[Pipeline] { (Branch: Matrix - NODE_VERSION = '22')
[Pipeline] { (Branch: Matrix - NODE_VERSION = '24')
error: no such object: node:22-trixie
+ node --version
v24.22.2
Test Files 2 passed (2)
Tests 4 passed (4)
+ node --version
v22.23.4
Test Files 2 passed (2)
Tests 4 passed (4)
Finished: SUCCESS
error: no such object satırı bir hata değil: Docker Pipeline image'ın yerelde olup olmadığını docker inspect ile kontrol ediyor, yoksa çekiyor. İki eksen olsaydı (ör. Node sürümü ve tarayıcı) matrix bütün kombinasyonları üretir; excludes ile istemediğin kombinasyonları çıkarırsın. Matrix'in bedeli executor: her hücre ayrı bir agent ister.
Windows ve IIS'e Deploy
Kurumsal ortamlarda hedef sık sık Windows ve IIS oluyor. Akış aynı (paketi al, uygulamayı durdur, dosyaları değiştir, başlat, health check), ama araçlar farklı: windows label'lı bir agent, powershell adımı ve IIS'in app_offline.htm mekanizması. ASP.NET Core Module, site kökünde app_offline.htm dosyasını görünce uygulamayı durdurur ve DLL'lerin (Dynamic-Link Library) kilidini bırakır; dosyaları kopyalayıp app_offline.htm'i sildiğinde uygulama yeni sürümle açılır. Bu makinede Windows olmadığı için aşağıdaki stage çalıştırılmadı; yalnız yapıyı gösteriyor:
stage('Deploy to IIS') {
when { branch 'main' }
agent { label 'windows && iis' }
steps {
unstash 'package'
powershell '''
$site = 'C:\\inetpub\\bookstore'
Set-Content -Path "$site\\app_offline.htm" -Value 'Deploying Bookstore'
tar -xzf "bookstore-$env:VERSION.tgz" -C $site
Remove-Item "$site\\app_offline.htm"
Invoke-WebRequest -UseBasicParsing http://localhost/health
'''
}
}
Web Deploy (msdeploy) daha gelişmiş bir alternatif. IIS tarafının ayrıntılarını, kilitli dosya sorunlarını ve Application Initialization'ı ilerideki Web Server'lar ve Hosting serisinin IIS yazılarında ele alacağız.
Üretimde Dikkat
Onay, deploy'un yerine geçmez. input bir insanın "evet" demesini kaydeder; ama o insanın neye evet dediğini (hangi sürüm, staging'de ne kadar test edildi) görebilmesi gerekir. Mesajda sürümü göster, build sayfasında staging sonucunu bırak.
Deploy credential'ını en dar yerde aç: sshagent yalnız deploy script'inin çevresinde. Sunucuda deploy kullanıcısının yetkisi de dar: dosya yazmak ve tek bir servisi yeniden başlatmak.
Rollback'i deneyin. releases/ klasöründe eski sürümler duruyor; symlink'i geri çevirmenin script'i ve bunu yapan bir Jenkins job'u, ihtiyaç duymadan önce hazır olsun. Eski sürümleri belli bir sayıdan sonra temizlemeyi de unutma. Blue-green ve canary gibi stratejileri ilerideki Deployment Stratejileri serisinde konuşacağız.
Sık Düşülen Tuzaklar
Onay beklerken executor tutmak. Agent'lı bir stage içinde input adımı çağırmak, bekleme boyunca executor'ı kilitler; declarative input direktifi ya da agent'sız bir stage kullan.
Kilidin sırayı garanti ettiğini sanmak. Yukarıda gördüğün gibi eski build yeniden sonra deploy edebiliyor; milestone olmadan staging'de eski sürüm kalabilir.
Deploy'da yeniden build etmek. Staging'e giden paketle production'a giden paket farklı olur.
Health check'siz deploy. Dosyaları kopyalamak uygulamanın açıldığı anlamına gelmez; ilk istekte 502 dönen bir deploy'u "başarılı" sayarsın.
Timeout'suz input. Onaylanmayan build'ler aylarca bekler, her biri bir build kaydı ve bazen bir kilit tutar.
Küçük Bir Deneme
★☆☆ Kendi pipeline'ına agent'sız bir onay stage'i ekle, submitter'ı kendi kullanıcın dışında birine ayarla ve kendi kullanıcınla onaylamayı dene. Ne oluyor?
★★☆ lock örneğini önce milestone'suz, sonra inversePrecedence ve milestone ile üç eşzamanlı build'le çalıştır; hangi build'in hangi sırayla deploy ettiğini tablo hâlinde yaz.
★★★ ci/rollback.sh adlı bir script yaz: releases/ altındaki bir önceki sürümü bulsun, current'ı ona çevirsin, servisi yeniden başlatsın ve health check yapsın. Sonra bunu parametreli bir Jenkins job'una bağla.
Jenkinsfile büyüdü: API, Web, paketleme, iki ortama deploy ve onay. Bookstore'un başka uygulamaları da aynı adımlara ihtiyaç duyarsa bu kodu kopyalamak yerine paylaşmak isteyeceksin. Bir sonraki yazının konusu bu: Shared Library, vars/ ve src/.