Lonjakan HTTP 502 Bad Gateway yang terjadi saat rolling update aplikasi Next.js di Kubernetes hampir selalu bersumber dari race condition pada siklus hidup Pod. Masalah ini umum ditemukan pada deployment Next.js dengan mode output: 'standalone' yang langsung melayani traffic HTTP dari Ingress tanpa sinkronisasi terminasi jaringan.

Akar Masalah: Desinkronisasi Endpoint Lifecycle di Kubernetes

Ketika deployment diperbarui, Kubernetes API Server memulai dua proses terminasi secara paralel dan asinkron pada Pod lama:

  1. Penghapusan Endpoint Jaringan: Endpoint controller menghapus IP Pod dari resource Endpoints atau EndpointSlice. Komponen Ingress controller (seperti Ingress-NGINX) dan kube-proxy membaca perubahan ini lalu memperbarui tabel routing lokal (iptables, IPVS, atau konfigurasi upstream Ingress).
  2. Pengiriman Sinyal Terminasi: Kubelet mengirim sinyal SIGTERM langsung ke PID 1 di container aplikasi untuk memerintahkan shutdown.

Propagasi pembaruan routing Ingress dan iptables memerlukan waktu antara 1 hingga 5 detik tergantung beban cluster. Jika container Next.js langsung menghentikan proses atau berhenti menerima koneksi begitu menerima SIGTERM, paket HTTP yang masih diarahkan oleh Ingress ke IP Pod lama akan ditolak dengan TCP Reset (RST). Akibatnya, Ingress mengembalikan respons HTTP 502 Bad Gateway ke pengguna akhir.

Analisis Log Ingress dan Bukti Metrik

Identifikasi masalah dapat diverifikasi melalui log Ingress Controller dan metrik koneksi TCP. Pada Ingress-NGINX, log akses akan mencatat kode status 502 dengan variabel $upstream_response_time bernilai sangat rendah atau berupa tanda hubung (-).

# Contoh log akses Ingress-NGINX
[24/Feb/2025:10:15:32 +0000] "GET /dashboard HTTP/2.0" 502 157 "https://example.com/" "Mozilla/5.0..." 12 0.001 [default-nextjs-app-3000] [] 10.244.2.45:3000: 0.001 502 0.001

Periksa log error Ingress untuk mendeteksi kegagalan koneksi upstream:

2025/02/24 10:15:32 [error] 1124#1124: *894125 connect() failed (111: Connection refused) while connecting to upstream, client: 192.168.1.1, server: example.com, request: "GET /dashboard HTTP/2.0", upstream: "http://10.244.2.45:3000/dashboard"

Pesan connect() failed (111: Connection refused) mengonfirmasi bahwa Pod target telah menutup soket TCP port 3000 sementara Ingress masih menganggap Pod tersebut sehat dan siap menerima traffic.

Solusi 1: Mengulur Terminasi Menggunakan preStop Hook

Kubelet mengeksekusi preStop hook sebelum mengirimkan sinyal SIGTERM ke container. Dengan menyisipkan jeda waktu (sleep delay), Ingress controller dan kube-proxy memiliki jendela waktu yang cukup untuk mencabut IP Pod dari upstream pool sebelum aplikasi mulai dimatikan.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nextjs-standalone
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: nextjs
          image: my-registry/nextjs-app:v1.2.0
          ports:
            - containerPort: 3000
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 15"]
      # Pastikan terminationGracePeriodSeconds lebih besar dari sleep preStop
      terminationGracePeriodSeconds: 45

Catatan: Nilai sleep 15 memberi waktu propagasi 15 detik. Pastikan terminationGracePeriodSeconds disetel minimal 30–45 detik agar Kubernetes tidak membunuh container secara paksa menggunakan SIGKILL sebelum proses shutdown selesai.

Solusi 2: Sinkronisasi keepAliveTimeout Node.js

Masalah 502 sekunder pasca-deploy muncul ketika reverse proxy mempertahankan koneksi HTTP Keep-Alive, namun server Node.js memutuskan koneksi tersebut lebih awal secara sepihak. Secara default, Node.js HTTP server memiliki keepAliveTimeout sebesar 5 detik, sedangkan banyak Ingress Controller atau Load Balancer (seperti AWS ALB) mempertahankan koneksi idle hingga 60 detik.

Jika client mengirim request baru bersamaan dengan penutupan koneksi oleh Node.js, terjadi race condition yang memicu TCP Reset. Konfigurasikan server Next.js agar nilai keepAliveTimeout lebih besar dari idle timeout upstream proxy.

Untuk project dengan custom server atau wrapper standalone (server.js):

const http = require('http');
const next = require('next');

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  const server = http.createServer((req, res) => {
    handle(req, res);
  });

  // Set timeout melebihi idle timeout Ingress (misal Ingress 60s)
  server.keepAliveTimeout = 65000;
  server.headersTimeout = 66000;

  server.listen(3000, () => {
    console.log('Next.js standalone listening on port 3000');
  });

  // Graceful shutdown
  process.on('SIGTERM', () => {
    console.log('SIGTERM received. Starting graceful close...');
    server.close((err) => {
      if (err) {
        console.error('Error during close:', err);
        process.exit(1);
      }
      console.log('HTTP server successfully closed');
      process.exit(0);
    });
  });
});
Peringatan Arsitektur: Nilai headersTimeout harus selalu diatur lebih tinggi dari keepAliveTimeout sesuai spesifikasi internal Node.js untuk mencegah crash runtime akibat header parsing failure.

Solusi 3: Penanganan Sinyal SIGTERM yang Benar di Dockerfile

Pada Next.js standalone bawaan (tanpa custom server), container dijalankan dengan node server.js. Pastikan perintah eksekusi di Dockerfile menggunakan format exec form, bukan shell form, agar sinyal SIGTERM tidak tertahan di subshell.

# BENAR: Sinyal diteruskan langsung ke proses Node.js
CMD ["node", "server.js"]

# SALAH: Sinyal terperangkap di /bin/sh, node tidak menerima SIGTERM
CMD npm start
# atau
CMD node server.js

Template Postmortem Insiden 502

Gunakan format ringkas berikut untuk dokumentasi pasca-insiden:

  • Ringkasan Masalah: Peningkatan spike error HTTP 502 sebesar 4.2% selama 3 menit pada saat rolling deploy aplikasi Next.js rilis v1.1.8.
  • Akar Masalah (Root Cause): Pod lama menerima SIGTERM dan langsung terminasi sebelum Ingress-NGINX selesai memperbarui upstream pool. Request baru tetap dirutekan ke IP Pod yang sudah mati.
  • Dampak: 320 request gagal dengan respons 502 Bad Gateway di endpoint transaksi.
  • Tindakan Koreksi: Menambahkan preStop hook sleep 15 dan menaikkan terminationGracePeriodSeconds menjadi 45 detik pada spec Deployment. Menyesuaikan keepAliveTimeout ke 65 detik.

Checklist Verifikasi Zero-Downtime Rollout

Sebelum menandai perbaikan selesai, lakukan verifikasi menggunakan traffic generator (misal: hey atau k6) selama proses deploy berlangsung:

  1. Jalankan load test konstan: hey -z 60s -q 20 -c 10 https://app.example.com/api/health.
  2. Lakukan rolling update: kubectl rollout restart deployment/nextjs-standalone.
  3. Pastikan tidak ada respons non-200 pada output load test: metrik Status code 200 harus mencapai 100%.
  4. Pantau log Ingress: pastikan tidak ada baris Connection refused atau 502 saat pod lama berpindah ke status Terminating.