Mert Özen Her Satırda Daha İleri
DevOps Deployment Stratejileri — #04

Blue-Green Deployment: Nginx ile İki Environment, App Service Slot'ları ve Anında Geri Dönüş

Mert Özen 8 Kas 2026 10 dk 2 görüntülenme
Blue-Green Deployment: Nginx ile İki Environment, App Service Slot'ları ve Anında Geri Dönüş

Blue-green yeni sürümü boştaki ortama kurup smoke test'ten geçirir ve trafiği tek reload ile çevirir; geri dönüş de aynı reload'dur. Nginx'te preview portu, sürüm kontrollü smoke test, switch ve geri dönüşte hatasız probe, reload sırasında blue'da tamamlanan 4 sn'lik istek; App Service slot swap'ının warm-up ve slot setting tuzakları, Kubernetes'te Service selector değişimi ve ortak veritabanının getirdiği sınır.

Sürüm notu: Bu yazıdaki örnekler .NET 10 (SDK 10.0.402), Nginx 1.30, Docker 29 ve kind 0.33 ile Kubernetes 1.37 üzerinde denenmiştir. Azure App Service bölümü dokümana dayanır, çalıştırılmamıştır.

Önce Tahmin Et

Blue ortamında 1.0.0, green ortamında 1.1.0 çalışıyor. Nginx'e "artık green'e git" diyorsun ve config'i reload ediyorsun. Reload'dan bir saniye önce blue'ya gönderilmiş, dört saniye sürecek bir istek var. O istek ne olur: yarıda mı kesilir, green'e mi taşınır, yoksa blue'da tamamlanır mı?

İkinci soru: Switch'i yaptın, beş dakika sonra green'de bir hata gördün. Geri dönmek ne kadar sürer ve neye bağlı?

Blue-Green'in Fikri

Blue-green'de production iki eş ortamdan oluşur. Biri canlı trafiği alır, diğeri boştadır. Yeni sürüm boştaki ortama tamamen kurulur, ısınır ve test edilir. Hazır olduğunda router (load balancer, reverse proxy, DNS (Domain Name System)) trafiği tek hamlede yeni ortama çevirir. Eski ortam olduğu gibi bekler. Bir sorun çıkarsa trafik aynı hamleyle geri döner. Martin Fowler'ın 2010 tarihli "BlueGreenDeployment" yazısındaki tarif bu; renklerin bir anlamı yok, önemli olan "canlı" ve "boşta" rollerinin her deploy'da yer değiştirmesi.

Rolling'le karşılaştırınca kazanç iki tane. Karışık sürüm penceresi switch anına iner, çünkü kullanıcılar ya tamamen blue'yu ya tamamen green'i görür. Geri dönüş de deploy değil bir config değişikliğidir, saniyeler sürer. Bedeli de iki tane: switch süresince iki tam ortam için kapasite gerekir ve iki ortam genelde aynı veritabanını paylaşır. Bu ikincisi blue-green'in asıl zor tarafı; yazının sonunda ona döneceğiz.

04-bluegreen-tr.png

Tek Makinede İki Ortam ve Nginx

Planlanan senaryo tek bir Linux VM (Virtual Machine) üzerinde iki systemd unit'i: bookstore-blue 5001'de, bookstore-green 5002'de. Bu yazıda aynı düzeni iki Docker container'ıyla kurdum (depwork-blue ve depwork-green, ikisi de 8080'de, aynı Docker ağında). Nginx açısından fark yok; yalnız upstream'deki adres değişiyor. .NET uygulamasını systemd service olarak çalıştırmanın ayrıntısı ilerideki Web Server'lar ve Hosting serisinde.

Nginx iki port dinliyor. 80 canlı trafik için, 8081 ise "preview": boştaki ortama yalnız bizim smoke test'imizin eriştiği bir kapı.

events {}

http {
    resolver 127.0.0.11 valid=1s ipv6=off;
    log_format live '$msec $status $upstream_http_x_app_version $request_time';
    access_log /var/log/nginx/bookstore.log live;

    include /etc/nginx/bookstore/upstreams.conf;

    server {
        listen 80;
        location / {
            proxy_pass http://bookstore_live;
        }
    }

    server {
        listen 8081;
        location / {
            proxy_pass http://bookstore_preview;
        }
    }
}

Hangi rengin canlı olduğunu upstreams.conf belirliyor ve onu switch script'i yazıyor:

#!/usr/bin/env bash
# switch.sh COLOR: canlı trafiği COLOR'a gönderir, diğer rengi preview olarak bırakır
set -euo pipefail
live=$1
if [ "$live" = blue ]; then idle=green; else idle=blue; fi
cat > nginx/bookstore/upstreams.conf <<CONF
upstream bookstore_live {
    zone bookstore_live 64k;
    server depwork-$live:8080 resolve;
}

upstream bookstore_preview {
    zone bookstore_preview 64k;
    server depwork-$idle:8080 resolve;
}
CONF
docker exec depwork-nginx nginx -t -q
docker exec depwork-nginx nginx -s reload 2>/dev/null
echo "$live" > nginx/bookstore/LIVE
echo "$(date +%T) live=$live preview=$idle"

nginx -t config'i doğrulamadan reload'a izin vermiyor. Bozuk bir config ile reload denersen Nginx eski config'le çalışmaya devam eder, ama set -e sayesinde script hata verip durur ve bunu fark edersin.

Önce Smoke Test, Sonra Switch

Deploy boştaki ortama yapılır. Canlı trafik hâlâ blue'da:

docker stop depwork-green && docker rm depwork-green
docker run -d --name depwork-green --network depwork-net --stop-timeout 40 \
    -e WARMUP_SECONDS=2 depwork-bookstore-api:1.1.0
./smoke.sh http://localhost:18602 1.1.0
#!/usr/bin/env bash
# smoke.sh BASE_URL EXPECTED_VERSION: switch'ten önce asgari kontroller
set -euo pipefail
base=$1
expected=$2
check() {
    local code
    code=$(curl -s -o /dev/null -w '%{http_code}' -m 3 "$base$1")
    echo "$1 -> $code"
    [ "$code" = 200 ]
}
check /health/ready
check /api/books
actual=$(curl -s -m 3 "$base/api/version" | sed 's/.*"version":"\([^"]*\)".*/\1/')
echo "version -> $actual"
[ "$actual" = "$expected" ]

Smoke test'in çıktısı:

/health/ready -> 200
/api/books -> 200
version -> 1.1.0

Smoke test kasıtlı olarak kısa: readiness, en kritik okuma endpoint'i ve sürümün doğru olduğu. Sürüm kontrolünü atlama. Preview'ün yanlış ortama baktığı ya da image tag'inin yanlış basıldığı bir hatayı yalnız o yakalar. Aynı script'i switch'ten sonra çalıştırdığımda (preview artık eski blue'yu gösteriyordu) sonuç şuydu:

/health/ready -> 200
/api/books -> 200
version -> 1.0.0
exit=1

Sağlık kontrolleri geçti ama sürüm tutmadı. Bu kontrol olmasaydı "green sağlam" diye trafiği yanlış ortama çevirebilirdin. Serinin son yazısındaki pipeline'da tam bu hata bir kez yaşandı ve tam bu satır sayesinde yakalandı.

Şimdi switch. Arka planda saniyede on istek atan probe çalışıyor ve bir de dört saniye süren bir istek blue'ya gönderilmiş durumda:

./switch.sh green
curl -s localhost:18600/api/version
06:30:47 live=green preview=blue
{"version":"1.0.0","instance":"96b0ba51efa0"}

İlginç bir ayrıntı: switch script'i bittikten hemen sonra atılan istek hâlâ 1.0.0 döndü. nginx -s reload master process'e sinyal gönderip hemen döner. Yeni worker'ların açılması ve eskilerin dinlemeyi bırakması asenkron gerçekleşir. Birkaç milisaniyelik bu pencere switch'in "atomik" olmadığını hatırlatıyor. Switch'ten sonraki doğrulamayı tek bir istekle değil, kısa bir tekrar döngüsüyle yap.

Dört saniyelik istek ise şöyle bitti:

{"version":"1.0.0","instance":"96b0ba51efa0","waitedMs":4000} -> 200 in 4.005074s

Baştaki ilk sorunun cevabı: istek blue'da tamamlandı. Reload sırasında eski worker'lar ellerindeki bağlantıları bitirene kadar yaşar; yeni bağlantılar yeni worker'lara, yani yeni config'e gider. Bu nedenle blue'yu switch'ten hemen sonra kapatmamalısın. İçeride hâlâ iş bitiren bağlantılar var.

Anında Geri Dönüş

Switch'ten altı saniye sonra geri döndüm, üç saniye sonra tekrar green'e geçtim:

06:30:53 live=blue preview=green
06:30:56 live=green preview=blue

Probe'un aynı zaman aralığındaki çıktısı (kısaltılmış):

   8 06:30:45 200 1.0.0
   9 06:30:46 200 1.0.0
   2 06:30:47 200 1.0.0
   6 06:30:47 200 1.1.0
   9 06:30:48 200 1.1.0
   8 06:30:52 200 1.1.0
   5 06:30:53 200 1.0.0
   4 06:30:53 200 1.1.0
   8 06:30:54 200 1.0.0
   4 06:30:56 200 1.0.0
   3 06:30:56 200 1.1.0
   9 06:30:57 200 1.1.0

Bütün istekler 200. Her switch, isteklerin bir saniye içinde diğer sürüme geçmesiyle sonuçlandı. İkinci sorunun cevabı da bu: geri dönüş, blue hâlâ ayakta ve sağlıklı olduğu sürece bir reload kadar sürer. Bu "olduğu sürece" kısmı önemli. Blue'yu kapattıysan, üzerine yeni bir sürüm koyduysan ya da veritabanı blue'nun anlamadığı bir şekle girdiyse, geri dönüş artık bir reload değil yeni bir deploy olur.

Azure App Service Deployment Slot'ları

Yönetilen platformlarda aynı fikir hazır gelir. Azure App Service'te her slot kendi host adına sahip canlı bir uygulamadır. Yeni sürümü staging slot'una deploy edersin, swap ile production'a alırsın. Bu bölümü çalıştırmadım; Microsoft Learn'deki "Set up staging environments" dokümanına göre önemli noktalar şunlar:

  • Slot'lar Standard, Premium ve Isolated tier'larda var. Slot başına ek ücret yok ama hepsi aynı App Service planının kaynaklarını paylaşır.
  • Swap sırasında hedef slot'un slot'a özgü ayarları kaynak slot'a uygulanır, kaynak slot'un instance'ları yeniden başlar ve ısınır. Bu süre boyunca production etkilenmez. Hepsi ısınınca routing kuralları değiştirilir.
  • Varsayılan warm-up, uygulamanın köküne bir HTTP (Hypertext Transfer Protocol) isteği atar ve herhangi bir cevap gelirse instance'ı ısınmış sayar. 500 dönen bir uygulama da "ısınmış" sayılır. WEBSITE_SWAP_WARMUP_PING_PATH'i readiness endpoint'ine, WEBSITE_SWAP_WARMUP_PING_STATUSES'i 200'e ayarlamak bu yüzden önemli.
  • "Deployment slot setting" olarak işaretlenen app setting ve connection string'ler swap'ta yer değiştirmez. Staging'in veritabanına bakan bir connection string'i işaretlemeyi unutursan swap sonrası production staging veritabanına bağlanır.
  • Swap with preview, swap'ı ilk adımdan sonra durdurur. Böylece kaynak slot'u production ayarlarıyla doğrulayabilirsin. Swap bittikten sonra eski production instance'ları staging'e geçer ve son adımda geri dönüştürülür. Dokümana göre uzun süren işlemler bu sırada yarıda kalır.

Geri dönüş, aynı iki slot'u tekrar swap etmektir. Burada da Nginx örneğindeki kural geçerli: staging slot'una yeni bir sürüm deploy ettiğin an geri dönüş yolun kapanır.

Kubernetes'te Service Selector Değişimi

Kubernetes'te blue-green için ek bir araca gerek yok. İki Deployment (bookstore-blue ve bookstore-green) farklı color label'ıyla çalışır ve Service selector'ü hangisini seçerse trafik ona gider:

apiVersion: v1
kind: Service
metadata:
  name: bookstore-api
spec:
  selector:
    app: bookstore-api
    color: blue
  ports:
    - port: 80
      targetPort: 8080

kind cluster'ında bir curl pod'undan Service'e istek atıp selector'ü değiştirdim:

kubectl exec curl -- sh -c 'for i in 1 2 3 4; do curl -s http://bookstore-api/api/version; echo; done'
kubectl patch service bookstore-api -p '{"spec":{"selector":{"color":"green"}}}'
kubectl exec curl -- sh -c 'for i in 1 2 3 4; do curl -s http://bookstore-api/api/version; echo; done'
{"version":"1.0.0","instance":"bookstore-blue-6c749fc4fb-6jwqh"}
{"version":"1.0.0","instance":"bookstore-blue-6c749fc4fb-6jwqh"}
{"version":"1.0.0","instance":"bookstore-blue-6c749fc4fb-mgvpv"}
{"version":"1.0.0","instance":"bookstore-blue-6c749fc4fb-6jwqh"}
service/bookstore-api patched
{"version":"1.1.0","instance":"bookstore-green-59cffbc476-ncrqn"}
{"version":"1.1.0","instance":"bookstore-green-59cffbc476-4748d"}
{"version":"1.1.0","instance":"bookstore-green-59cffbc476-ncrqn"}
{"version":"1.1.0","instance":"bookstore-green-59cffbc476-4748d"}

Patch'ten sonraki ilk istek green'e gitti. Service'in EndpointSlice'ları yeni pod'ları gösterdiği an kube-proxy kuralları güncellenir. Ancak açık TCP (Transmission Control Protocol) bağlantıları, örneğin keep-alive ile açık tutulan HTTP bağlantıları, eski pod'a gitmeye devam edebilir. Service ve EndpointSlice'ın çalışma şekli ilerideki Kubernetes serisinde. Argo Rollouts'un blueGreen stratejisi ise preview Service'i, otomatik promotion ve eski ReplicaSet'i bir süre ayakta tutma (scaleDownDelaySeconds) gibi adımları bu fikrin üzerine ekler.

Ortak Veritabanı ve Diğer Gerçekler

Blue-green trafiği iki ortama ayırır, veriyi ayırmaz. İki ortamın ayrı veritabanları olsaydı switch anında green'in veritabanı blue'nun son dakikalarındaki siparişleri bilmezdi. Bu yüzden pratikte iki ortam aynı veritabanına bağlanır. Bunun sonucu net: green'i deploy etmeden önce çalıştırdığın migration, blue hâlâ canlıyken uygulanır ve geri dönersen blue o şemayla çalışmak zorunda kalır. Yani blue-green, geriye uyumlu migration olmadan geri dönüş garantisi vermez. Serinin altıncı yazısındaki expand-contract tam olarak bunun için.

Uzun yaşayan bağlantılar ikinci gerçek. WebSocket ve SignalR bağlantıları reload'da yeni ortama taşınmaz; eski Nginx worker'ı onları kapanana kadar taşır. Nginx'in worker_shutdown_timeout ayarı bu bekleme için bir üst sınır koyar. Bağlantılar en sonunda kapanınca istemci yeniden bağlanır ve bu kez green'e gider. Yine de bu bağlantılar dakikalarca blue'da kalabilir, bu yüzden blue'yu kapatmadan önce aktif bağlantı sayısına bak.

Üçüncüsü maliyet. Switch penceresinde iki tam ortam gerekir. Bulutta green'i yalnız deploy süresince ayağa kaldırıp switch'ten bir süre sonra kapatmak maliyeti düşürür, ama bu "bir süre" geri dönüş penceren demektir. Onu bilinçli seç; ilk saatte bulunan hataların oranı genelde bu kararı verir.

Dördüncüsü arka plan işleri. Her iki ortamda da çalışan bir scheduled job ya da queue consumer'ı varsa boştaki ortam da iş yapıyor demektir. Blue-green'de "boşta" yalnız HTTP trafiği için geçerlidir. Background job'ları ya rengine göre açıp kapatırsın ya da distributed lock ile tek instance'a sınırlarsın.

Üretimde Dikkat

Blue-green'in üç kritik sayısı var. Smoke test süresi, deploy'dan switch'e kadar geçen süreyi belirler. Switch sonrası ilk birkaç dakikadaki hata oranı geri dönüş kararını verir; bu metrik switch'ten önceki blue'yla karşılaştırılmalı. Eski ortamın ayakta kaldığı süre ise geri dönüş penceren.

Switch adımı insan eliyle yapılıyorsa, geri dönüş komutunu da önceden hazır tut ve bir kez gerçekten çalıştır. "Gerekirse döneriz" denen planlarda en sık görülen sorun, geri dönüş komutunun ilk kez bir incident sırasında çalıştırılmasıdır.

Sık Düşülen Tuzaklar

  • Switch'ten hemen sonra blue'yu kapatmak. Hem in-flight istekler kesilir hem de geri dönüş yolu kaybolur. En az bir doğrulama penceresi boyunca blue ayakta kalmalı.
  • Smoke test'i canlı porta karşı koşmak. Smoke test boştaki ortamı switch'ten önce doğrulamalı; canlı porttan geçen bir test eski ortamı test eder.
  • Migration'ı switch'le aynı adıma koymak. Geriye uyumsuz bir migration geri dönüşü anlamsız kılar.
  • App Service'te slot'a özgü connection string'i işaretlemeyi unutmak. Swap sonrası production yanlış veritabanına bağlanır.
  • Arka plan işlerini unutmak. Boştaki ortamdaki bir consumer, deploy edilmekte olan yarım sürümle mesaj işleyebilir.

Küçük Bir Deneme

★☆☆ switch.sh'den hemen sonra 20 istek atıp kaç tanesinin eski sürüme gittiğini say. Aynı denemeyi nginx -s reload ile sleep 1 arasında tekrarla.

★★☆ Blue'ya bir WebSocket bağlantısı aç (örneğin websocat ile), switch yap ve bağlantının ne kadar süre blue'da kaldığını gözle. worker_shutdown_timeout 10s; ekleyip tekrarla.

★★★ switch.sh'yi geri dönüşü otomatik yapacak şekilde genişlet: switch'ten sonra 30 saniye boyunca canlı porttan istek at ve 5xx oranı %2'yi aşarsa önceki renge dön. Bunu serinin sekizinci yazısında gerçek bir hata ile koşturacağız.

Blue-green trafiği tek hamlede çeviriyor, ama o hamle bütün kullanıcıları aynı anda yeni sürüme gönderiyor. Bir sonraki yazıda bu hamleyi yüzdelere bölüyoruz: Nginx'te weight ve split_clients ile canary release, metriklerle karar vermek ve Argo Rollouts ile otomatik analiz.