Error HTTP 502 Bad Gateway saat proses rolling deploy aplikasi Django di Kubernetes biasanya disebabkan oleh hilangnya proses backend sebelum router jaringan berhenti mengarahkan traffic. Masalah ini bukan disebabkan oleh bug pada logika aplikasi Django, melainkan race condition pada siklus terminasi Pod dan propagasi pembaruan endpoint di tingkat klaster.

Akar Masalah: Race Condition Terminasi Pod

Ketika deployment diperbarui, Kubernetes memulai pembuatan Pod baru dan mematikan Pod lama secara paralel. Proses terminasi Pod lama memicu dua rantai peristiwa independen yang berjalan secara asinkron:

  1. Penghapusan Endpoint dari Jaringan: Endpoint controller mendeteksi Pod masuk ke fase Terminating, menghapus IP Pod dari resource Endpoints / EndpointSlice, lalu mengabari Ingress controller, Kube-proxy, dan iptables/IPVS node untuk berhenti merutekan traffic.
  2. Terminasi Kontainer: Kubelet secara bersamaan mengirim sinyal SIGTERM ke kontainer utama aplikasi (Gunicorn).

Propagasi aturan iptables/IPVS ke seluruh worker node membutuhkan waktu antara 1 hingga 5 detik tergantung ukuran klaster. Jika Gunicorn menerima SIGTERM dan langsung menutup socket koneksi atau mematikan worker sementara Ingress controller masih mengirimkan request baru, Ingress akan menerima respon TCP RST atau Connection Refused. Hasilnya adalah status 502 Bad Gateway pada sisi klien.

Desain Health Check: Pemisahan Liveness dan Readiness Probe

Kesalahan umum dalam konfigurasi probe Django adalah menyatukan logika liveness dan readiness, atau memeriksa koneksi database pada endpoint liveness. Jika database mengalami fluktuasi latensi sesaat, Kubelet akan salah mendiagnosis Pod mati dan memicunya untuk restart massal (cascading failure).

Pisahkan kedua probe tersebut dengan kriteria:

  • Liveness Probe: Memastikan web server masih merespons loop HTTP. Jangan pernah menyertakan query database atau integrasi eksternal.
  • Readiness Probe: Memastikan aplikasi siap menerima traffic (koneksi database dan cache aktif).

Implementasi di Django

# urls.py
from django.urls import path
from .views import health_live, health_ready

urlpatterns = [
    path("healthz/live/", health_live, name="health-live"),
    path("healthz/ready/", health_ready, name="health-ready"),
]
# views.py
from django.http import HttpResponse, JsonResponse
from django.db import connection

def health_live(request):
    # Cek minimal: proses Gunicorn berjalan dan event loop responsif
    return HttpResponse("OK", status=200)

def health_ready(request):
    # Cek dependensi: verifikasi ketersediaan database
    try:
        with connection.cursor() as cursor:
            cursor.execute("SELECT 1;")
        return JsonResponse({"status": "ready"}, status=200)
    except Exception as exc:
        return JsonResponse({"status": "unhealthy", "reason": str(exc)}, status=503)

Konfigurasi Graceful Shutdown pada Gunicorn

Secara default, saat Gunicorn menerima sinyal SIGTERM, master process akan meneruskan sinyal tersebut ke worker dan langsung menutup listening socket. Agar request yang sedang berlangsung (in-flight requests) dapat selesai diproses, Gunicorn membutuhkan konfigurasi graceful_timeout.

Konfigurasi gunicorn.conf.py

import multiprocessing

bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "gthread"
threads = 4

# Waktu tunggu maksimal untuk menyelesaikan in-flight requests setelah SIGTERM
graceful_timeout = 30

# Timeout eksekusi request normal
timeout = 60

# Keep-alive HTTP connection timeout
keepalive = 5
Catatan: Parameter graceful_timeout harus disesuaikan dengan latensi maksimal endpoint Django Anda (misalnya: file export atau batch processing ringan).

Sinkronisasi Kubernetes: preStop Hook dan terminationGracePeriodSeconds

Untuk mengatasi jeda waktu propagasi jaringan Kubernetes, kontainer harus menunda penerimaan sinyal SIGTERM. Hal ini dicapai menggunakan preStop lifecycle hook.

Ketika Pod beralih ke Terminating, hook preStop dieksekusi sebelum sinyal SIGTERM dikirim ke aplikasi. Perintah sleep memberi waktu bagi Kube-proxy dan Ingress controller untuk mencabut IP Pod dari tabel rute sebelum Gunicorn berhenti menerima traffic.

Konfigurasi Deployment Kubernetes

apiVersion: apps/v1
kind: Deployment
metadata:
  name: django-app
spec:
  replicas: 3
  template:
    spec:
      # Harus lebih besar dari (preStop sleep + gunicorn graceful_timeout)
      terminationGracePeriodSeconds: 60
      containers:
        - name: web
          image: django-app:latest
          command: ["gunicorn", "-c", "gunicorn.conf.py", "myproject.wsgi:application"]
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 15"]
          ports:
            - containerPort: 8000
          livenessProbe:
            httpGet:
              path: /healthz/live/
              port: 8000
            initialDelaySeconds: 10
            periodSeconds: 10
            timeoutSeconds: 2
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /healthz/ready/
              port: 8000
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 2

Perhitungan Timeline Terminasi:

  1. T = 0s: Rolling update dimulai. Pod masuk status Terminating. Endpoint controller menghapus Pod IP. Script sleep 15 pada preStop berjalan. Pod masih menerima sisa traffic jaringan yang belum tersinkronisasi.
  2. T = 15s: preStop selesai. Kube-proxy dan Ingress sudah bersih dari rute Pod lama. Kubelet mengirim sinyal SIGTERM ke Gunicorn.
  3. T = 15s - 45s: Gunicorn memproses in-flight requests hingga selesai (maksimal 30 detik berdasarkan graceful_timeout). Tidak ada request baru yang masuk.
  4. T = 45s: Semua worker selesai dan keluar secara bersih. Pod berhenti tanpa memicu error 502.
  5. T = 60s: Batas pengaman. Jika aplikasi masih menggantung, Kubelet mengirim SIGKILL paksa (ditentukan oleh terminationGracePeriodSeconds).

Observabilitas: Monitoring In-Flight Requests dan Metrik Ingress

Untuk memvalidasi bahwa strategi mitigasi bekerja tanpa menyisakan drop connection, amati metrik Ingress controller dan aplikasi selama deployment.

1. Monitoring HTTP 502 pada Prometheus

Gunakan kueri PromQL berikut pada Ingress NGINX untuk melacak error gateway selama rolling update:

sum(rate(nginx_ingress_controller_requests{status="502", ingress="django-app"}[1m])) by (status)

Nilai rate ini harus bernilai 0 stabil sepanjang proses deployment berjalan.

2. Pelacakan Active Connection Draining

Pastikan koneksi aktif menurun secara bertahap menuju 0 saat Pod terminasi melalui metrik runtime internal atau connection logging pada Ingress:

sum(nginx_ingress_controller_pod_active_connections{namespace="production"}) by (pod)

Checklist Deployment Pipeline

Terapkan validasi otomatis berikut pada pipeline CI/CD sebelum deployment dijalankan ke klaster produksi:

  • [ ] terminationGracePeriodSeconds didefinisikan secara eksplisit di spec Pod.
  • [ ] Nilai terminationGracePeriodSeconds > (durasi preStop sleep + gunicorn graceful_timeout).
  • [ ] Hook preStop terpasang dengan durasi sleep minimal 10–15 detik.
  • [ ] Endpoint liveness probe bebas dari ketergantungan database atau remote network call.
  • [ ] Endpoint readiness probe menguji kesiapan dependensi esensial dengan query ber-timeout rendah.
  • [ ] Sinyal OS diteruskan dengan benar dari init script kontainer ke Gunicorn (hindari pembungkusan gunicorn tanpa exec pada entrypoint shell script).