Tüm yazılar

blog

Docker ve Caddy ile elle blue-green dağıtım

Yayın tarihi

Blue-green bir yayın birkaç adım ve bolca hata yönetiminden oluşur. Docker ve Caddy için eksiksiz bir script ve yanlış yapılması kolay kısımların gerekçesi burada.

Düz bir yeniden başlatma neden istek düşürür

Eski konteyneri durdurup yenisini başlatmak bir boşluk bırakır: eskisi gitmiştir, yenisi hâlâ açılmaktadır. Arada gelen istekler proxy’den hata alır. Blue-green bu boşluğu, yeni sürümü eskisinin yanında çalıştırıp trafiği ancak yenisi hazır olunca taşıyarak kapatır.

Sıra

  1. 01Yeni rengi canlı olanın yanında, aynı ağda ve yayınlanmış port olmadan başlat.
  2. 02Docker sağlıklı diyene kadar bekle. O ana kadar ona hiçbir şey ulaşmaz.
  3. 03Yeni proxy yapılandırmasını yaz, doğrula, sonra zarif bir reload ile geçiş yap.
  4. 04Uygulamayı proxy üzerinden, bir kullanıcının geldiği yoldan kontrol et.
  5. 05Eski rengi ancak bundan sonra kaldır.

Uygulamayı test eden bir sağlık kontrolü

Yöntemin tamamı tek bir sinyale dayanır: Docker’ın sağlık durumu. Yalnızca portun açık olduğunu kanıtlayan bir kontrol, her isteğe hata dönen bir uygulamayı da memnuniyetle geçirir. Kontrolü gerçek iş yapan bir uç noktaya yönelt — örneğin veritabanını sorgulayan birine.

Dockerfile

HEALTHCHECK --interval=5s --timeout=3s --start-period=20s --retries=3 \
  CMD wget -qO- http://127.0.0.1:3000/healthz || exit 1

wget Alpine tabanlı imajlarda bulunur. Slim ya da distroless bir imajda imajda ne varsa onu kullan ya da uygulamanın içine küçük bir kontrol komutu ekle.

Varsaydığı kurulum

Caddy, web adlı bir Docker ağında caddy adlı bir konteyner olarak çalışıyor ve yapılandırma dizini /etc/caddy olarak bağlı. Tek dosyayı değil dizini bağla: script dosyayı değiştiriyor ve tek dosyalık bir bind mount Caddy’ye eskisini göstermeye devam eder.

Canlı konteynerin adı app-blue ya da app-green ve Caddyfile ona bu adla işaret ediyor. İlk seferde app-blue’yu elle başlat.

/opt/caddy/conf/Caddyfile

app.example.com {
	reverse_proxy app-blue:3000
}

Script

Script’i yerel bir Caddy ve registry’ye karşı dört durumda çalıştırdık: iyi bir yayın, hiç sağlıklı olmayan bir yayın, sağlıklı olup duman testinde düşen bir yayın ve ardından bir iyi yayın daha.

deploy.sh

#!/usr/bin/env bash
# Usage: ./deploy.sh ghcr.io/example/app:1.5.0
set -Eeuo pipefail

IMAGE=${1:?usage: deploy.sh IMAGE}
NETWORK=web
PROXY=caddy                       # the Caddy container
CONF=/opt/caddy/conf              # mounted at /etc/caddy inside it
ENV_FILE=/opt/app/.env
SMOKE_URL=https://app.example.com/healthz

wait_healthy() {
  local state
  for _ in $(seq 1 60); do
    state=$(docker inspect -f '{{.State.Running}} {{.State.Health.Status}}' "$1")
    case "$state" in
      "true healthy") return 0 ;;
      false* | *unhealthy) return 1 ;;
    esac
    sleep 2
  done
  return 1
}

live=$(grep -m1 -oE 'app-(blue|green)' "$CONF/Caddyfile") ||
  { echo "The Caddyfile must point at app-blue or app-green." >&2; exit 1; }
next=$([ "$live" = app-blue ] && echo app-green || echo app-blue)
switched=0

rollback() {
  echo "Deploy failed. $live keeps serving." >&2
  if [ "$switched" = 1 ]; then
    cp "$CONF/Caddyfile.previous" "$CONF/Caddyfile"
    docker exec "$PROXY" caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile || true
  fi
  rm -f "$CONF/Caddyfile.next"
  docker rm -f "$next" >/dev/null 2>&1 || true
}
trap rollback ERR

# 1. Start the new colour next to the live one.
docker pull "$IMAGE"
docker rm -f "$next" >/dev/null 2>&1 || true
docker run -d --name "$next" --network "$NETWORK" \
  --restart unless-stopped --env-file "$ENV_FILE" "$IMAGE" >/dev/null

# 2. Nothing reaches it until Docker reports it healthy.
wait_healthy "$next"

# 3. Validate the new proxy config before it replaces the running one.
cp "$CONF/Caddyfile" "$CONF/Caddyfile.previous"
sed "s/$live:/$next:/" "$CONF/Caddyfile.previous" > "$CONF/Caddyfile.next"
docker exec "$PROXY" caddy validate --config /etc/caddy/Caddyfile.next --adapter caddyfile

# 4. Switch. Caddy reloads gracefully; open connections finish.
switched=1
mv "$CONF/Caddyfile.next" "$CONF/Caddyfile"
docker exec "$PROXY" caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile

# 5. Check through the proxy, the way a user arrives.
curl -fsS --retry 3 --retry-delay 1 "$SMOKE_URL" >/dev/null

# 6. Only now retire the old colour.
trap - ERR
docker rm -f "$live" >/dev/null
echo "Live: $next ($IMAGE)"

Asıl mesele hata yolu

Elle yazılmış sürümlerin çoğu mutlu yolu doğru yapar ve hata durumunda ortalığı dağınık bırakır. Bu script neyi geri alacağına ne kadar ilerlediğine bakarak karar verir.

Yeni konteyner hiç sağlıklı olmazsa proxy’ye hiç dokunulmamıştır. Script adayı kaldırıp çıkar; canlı renk hiçbir şey olmamış gibi hizmet vermeye devam eder.

Geçişten sonra başarısız olursa — reload hata verirse ya da proxy üzerinden duman testi düşerse — önceki Caddyfile’ı geri koyar, yeniden yükler ve adayı kaldırır. Eski renk hiç durdurulmadığı için hemen yeniden hizmet verir.

Eski konteyner yalnızca duman testi geçtikten sonra kaldırılır. O satır çalışana kadar geri dönüş her zaman mümkün.

Her kontrolü geçip yine de yanlış çıkan bir yayını geri almak, aynı script’i önceki imaj etiketiyle çalıştırmaktır. Bu da bir blue-green geçişi olduğundan o da istek düşürmez.

Script’in senin yerine yapamayacağı iki şey

Birkaç saniye boyunca iki sürüm de aynı veritabanına karşı çalışır. Bu yüzden şema migration’ları eski sürümün dayanabileceği türden olmalı: sütunu, ona ihtiyaç duyan koddan önce ekle; eskisini bir yayın sonra kaldır.

docker run satırına volume eklersen iki renk de onları aynı anda bağlar. Çoğu web uygulaması için sorun değil; tek yazıcı olduğunu varsayan her şey için — tek süreçli bir iş zamanlayıcısı gibi — yanlış.

Otomatik sürüm

MihraOps bu sırayı domainde yayınlanan uygulamalar için yürütür: adayı aynı yapılandırmayla başlatır, sağlıklı demesini bekler, proxy’yi taşır ve sonraki adım başarısız olursa her adımı tersten geri alır. Hiç sağlıklı olmayan bir konteyner, ona hiçbir trafik ulaşmadan yayını başarısız kılar.

Blue-green yayınlar Pro’nun parçası. Tam sıra doküman sayfasında.

Sırada