karawaci.kode

← Semua snippet

Bash Lanjut Otomasi

Zero-downtime blue/green deploy via nginx

Deploy aplikasi tanpa downtime pakai nginx upstream switch. Blue serve traffic, green deploy + warmup, lalu swap. Rollback dalam 5 detik.

Dipublikasikan 19 Juni 2026

Tokopedia gak boleh down 1 menit pun di jam ramai. Blue/green deploy: jalankan 2 instance aplikasi (biru lagi serve, hijau standby), deploy versi baru ke hijau, smoke test, lalu switch nginx ke hijau. Kalau ada error, swap balik. Snippet ini script orchestrator dengan health check.

Kode

#!/usr/bin/env bash
# /opt/deploy/blue_green.sh
# Usage: ./blue_green.sh <new-image-tag>

set -euo pipefail

readonly APP_NAME="tokopedia-api"
readonly NEW_TAG="${1:?Pakai: $0 <image-tag>}"
readonly NGINX_UPSTREAM="/etc/nginx/upstream.conf"
readonly HEALTH_PATH="/healthz"
readonly WARMUP_REQUESTS=50
readonly HEALTH_TIMEOUT=120

# Port assignment
declare -A PORTS=([blue]=8081 [green]=8082)

log() {
  echo "[$(date -u +%H:%M:%S)] $*"
}

# Cek slot aktif sekarang berdasarkan upstream config
current_slot() {
  if grep -q "server 127.0.0.1:${PORTS[blue]}" "${NGINX_UPSTREAM}" 2>/dev/null; then
    echo "blue"
  else
    echo "green"
  fi
}

other_slot() {
  if [[ "$1" == "blue" ]]; then echo "green"; else echo "blue"; fi
}

deploy_to_slot() {
  local slot=$1
  local port=${PORTS[$slot]}
  local container="${APP_NAME}-${slot}"

  log "Stop container lama (kalau ada): ${container}"
  docker rm -f "${container}" 2>/dev/null || true

  log "Pull image: ${APP_NAME}:${NEW_TAG}"
  docker pull "${APP_NAME}:${NEW_TAG}"

  log "Start container ${container} di port ${port}"
  docker run -d \
    --name "${container}" \
    --restart unless-stopped \
    -p "127.0.0.1:${port}:8080" \
    --env-file /etc/${APP_NAME}/env \
    "${APP_NAME}:${NEW_TAG}"
}

wait_healthy() {
  local port=$1
  local deadline=$(( $(date +%s) + HEALTH_TIMEOUT ))

  log "Tunggu health check di port ${port}"
  while (( $(date +%s) < deadline )); do
    if curl -sf -o /dev/null -w "%{http_code}" \
        "http://127.0.0.1:${port}${HEALTH_PATH}" | grep -q "^200$"; then
      log "Health check OK di port ${port}"
      return 0
    fi
    sleep 2
  done

  log "ERROR: health check timeout di port ${port}"
  return 1
}

warmup() {
  local port=$1
  log "Warmup ${WARMUP_REQUESTS} request ke port ${port}"

  for i in $(seq 1 "${WARMUP_REQUESTS}"); do
    curl -sf -o /dev/null "http://127.0.0.1:${port}${HEALTH_PATH}" || true
  done
  log "Warmup selesai"
}

switch_nginx() {
  local target_slot=$1
  local target_port=${PORTS[$target_slot]}

  log "Switch nginx upstream ke ${target_slot} (port ${target_port})"

  cat > "${NGINX_UPSTREAM}" <<EOF
upstream app_backend {
  server 127.0.0.1:${target_port};
  keepalive 32;
}
EOF

  if ! nginx -t; then
    log "ERROR: nginx config invalid"
    return 1
  fi

  nginx -s reload
  log "Nginx reloaded — traffic sekarang ke ${target_slot}"
}

main() {
  local active
  active=$(current_slot)
  local standby
  standby=$(other_slot "${active}")

  log "Active: ${active}, deploy ke standby: ${standby}"

  deploy_to_slot "${standby}"

  if ! wait_healthy "${PORTS[$standby]}"; then
    log "Deploy gagal, container standby di-stop"
    docker rm -f "${APP_NAME}-${standby}"
    exit 1
  fi

  warmup "${PORTS[$standby]}"
  switch_nginx "${standby}"

  log "Grace period 30s sebelum stop slot lama"
  sleep 30

  log "Stop container slot lama: ${active}"
  docker rm -f "${APP_NAME}-${active}"

  log "=== Deploy ${NEW_TAG} selesai ke ${standby} ==="
}

main "$@"
#!/usr/bin/env bash
# /opt/deploy/rollback.sh — swap balik dalam 5 detik

set -euo pipefail

readonly NGINX_UPSTREAM="/etc/nginx/upstream.conf"

active=$(grep -oE "8081|8082" "${NGINX_UPSTREAM}" | head -1)

if [[ "${active}" == "8081" ]]; then
  rollback_port=8082
  rollback_name="green"
else
  rollback_port=8081
  rollback_name="blue"
fi

echo "Rollback ke ${rollback_name} (port ${rollback_port})"

if ! curl -sf -o /dev/null "http://127.0.0.1:${rollback_port}/healthz"; then
  echo "ERROR: target rollback tidak healthy. Manual intervention dibutuhkan."
  exit 1
fi

cat > "${NGINX_UPSTREAM}" <<EOF
upstream app_backend {
  server 127.0.0.1:${rollback_port};
  keepalive 32;
}
EOF

nginx -t && nginx -s reload
echo "Rolled back."

Pemakaian

# Deploy versi baru
sudo /opt/deploy/blue_green.sh v2.34.1

# Kalau ada error setelah switch, rollback
sudo /opt/deploy/rollback.sh

# Cek status
docker ps | grep tokopedia-api
# tokopedia-api-blue   v2.34.0   Up 2 days
# tokopedia-api-green  v2.34.1   Up 5 minutes
# /etc/nginx/sites-enabled/api.conf
upstream app_backend {
  server 127.0.0.1:8081;
  keepalive 32;
}

server {
  listen 443 ssl http2;
  server_name api.tokopedia-clone.id;

  location / {
    proxy_pass http://app_backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
    proxy_read_timeout 60s;
  }
}

Kapan dipakai

  • Production API yang butuh SLA tinggi.
  • Aplikasi yang punya schema migration backward compatible (tidak in-place breaking).
  • Service yang JIT-warmup-sensitive (JVM, .NET).
  • Stateful service yang gak boleh restart in-place (cache memory).

Catatan

  • DB migration harus backward compatible — slot lama masih bisa baca/tulis schema baru. Pakai expand-migrate-contract pattern.
  • Session sticky — kalau pakai in-memory session, switch bikin user logout. Pakai Redis / JWT supaya stateless.
  • Grace period sebelum stop slot lama — biar request inflight selesai. 30-60 detik biasanya cukup.
  • Health check beda dari readiness — health = “alive?”, readiness = “siap terima traffic?”. Warmup pengaruhi readiness, bukan health.
  • Resource 2x — slot ganda butuh memory dan CPU 2x sementara. Plan capacity.
  • Database connection pool — slot baru bikin pool baru. Total connection ke DB sementara 2x. Cek max_connections Postgres.

Untuk Kubernetes, pakai Service + Deployment label switch (atau pakai Argo Rollouts / Flagger). Pattern sama, cuma orchestrator-nya k8s.

# tags

deploymentnginxblue-greenzero-downtimedevops

Ditulis oleh Asti Larasati · 19 Juni 2026