L
Node.js • Literally Web ID

Zero-Downtime Deployment Node.js Microservices di Kubernetes

⏱️ 15 menit baca • 📅 19 Oct 2026 • 👁️ 4 views
Zero-Downtime Deployment Node.js Microservices di Kubernetes

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap arsitektur microservices modern, kemampuan untuk melakukan deployment pembaruan perangkat lunak tanpa mengalami gangguan layanan (zero-downtime deployment) bukan lagi sekadar kemewahan, melainkan syarat mutlak. Node.js, dengan model single-threaded event loop-nya yang sangat efisien untuk operasi I/O-bound, sering kali diandalkan untuk menangani beban konkurensi tinggi. Namun, ketika digabungkan dengan Kubernetes sebagai orkestrator kontainer, pengelolaan siklus hidup (lifecycle management) aplikasi Node.js menjadi titik kritis yang sering kali diabaikan oleh para engineer.

Bottleneck performa dan kegagalan ketersediaan layanan (availability) di dunia nyata biasanya terjadi pada detik-detik transisi saat proses rolling update berlangsung. Tanpa konfigurasi yang tepat, Kubernetes akan langsung mengarahkan lalu lintas (traffic) ke instance Pod baru yang sebenarnya belum sepenuhnya siap memproses permintaan, atau sebaliknya, langsung mematikan Pod lama yang masih memegang koneksi aktif (seperti Long-Polling atau WebSocket), mengakibatkan dropped connections, connection reset by peer, dan lonjakan HTTP 5xx errors bagi pengguna akhir.

Kondisi Sebelum vs Sesudah Penerapan

  • Sebelum Penerapan (Naive Deployment):

    • Pod baru langsung menerima traffic seketika setelah kontainer berjalan, tanpa memvalidasi apakah koneksi ke database, Redis, atau Message Queue telah mapan.
    • Pod lama langsung dimatikan (SIGKILL) saat perintah kubectl rollout dieksekusi, memutuskan koneksi TCP yang sedang aktif secara paksa.
    • Tingkat kegagalan request (error rate) melonjak drastis setiap kali CI/CD pipeline melakukan rilis versi baru.
    • Tim operasional harus melakukan rilis pada jam-jam sepi (maintenance window dini hari) untuk meminimalkan dampak gangguan.
  • Sesudah Penerapan (Zero-Downtime Microservices Arsitektur):

    • Penerapan Readiness Probe yang memastikan Node.js siap secara fungsional sebelum menerima traffic dari Kubernetes Service / Ingress.
    • Penerapan Liveness Probe yang mendeteksi kebocoran memori atau kebuntuan (deadlock) pada event loop dan melakukan auto-restart secara aman.
    • Implementasi Graceful Shutdown di level kode Node.js (SIGTERM handling) yang menutup koneksi HTTP server secara elegan, menyelesaikan in-flight requests, dan memutuskan koneksi database secara terstruktur.
    • Rilis dapat dilakukan kapan saja (Continuous Deployment di tengah hari bolong) tanpa ada satupun pengguna yang merasakan gangguan.

2. Konsep Inti & Deep Dive Arsitektur

Untuk memahami bagaimana zero-downtime tercapai, kita harus membedah siklus hidup Pod di Kubernetes dan bagaimana ia berinteraksi dengan proses Node.js di dalamnya. Berikut adalah diagram alur eksekusi saat terjadi proses pembaruan layanan (Rolling Update):

+-----------------------------------------------------------------------+
|                        KUBERNETES CLUSTER                             |+
|                                                                       |
|  [Ingress Controller / Service]                                       |
|                 |                                                     |
|                 v                                                     |
|  +-----------------------------+     +-----------------------------+  |
|  |      Old Pod (v1)           |     |      New Pod (v2)           |  |
|  |                             |     |                             |  |
|  |  1. Serving Traffic         |     |  1. Container Created       |  |
|  |  2. Receives SIGTERM        |     |  2. Startup Probe (Check)   |  |
|  |  3. Stop accepting new conn |     |  3. Readiness Probe (OK)    |  |
|  |  4. Finish in-flight req    |---->|  4. Added to Service Endpoints|
|  |  5. Close DB / Exit (0)     |     |  5. Serving Live Traffic    |  |
|  +-----------------------------+     +-----------------------------+  |
+-----------------------------------------------------------------------+

Mekanisme Kerja di Balik Layar

  1. Inisialisasi Pod (Startup & Readiness): Saat Pod baru dibuat, Kubernetes menjalankan Startup Probe. Jika aplikasi memerlukan waktu lama untuk memuat konfigurasi atau melakukan migrasi skema database, probe ini mencegah Liveness Probe membunuh kontainer terlalu dini. Setelah itu, Readiness Probe memverifikasi apakah endpoint HTTP aplikasi (misal: /healthz) mengembalikan status 200 OK. Hanya setelah probe ini sukses, IP Pod didaftarkan ke Endpoints dari Kubernetes Service.
  2. Pemutusan Alur (Termination Lifecycle): Ketika Pod lama menerima sinyal SIGTERM dari kubelet, Kubernetes secara otomatis mencabut IP Pod dari daftar Endpoints Service agar tidak ada lagi traffic baru yang masuk. Namun, proses Node.js di dalam kontainer masih hidup dan diberi waktu (berdasarkan parameter terminationGracePeriodSeconds, default 30 detik) untuk menyelesaikan seluruh permintaan yang sedang berjalan (in-flight requests).
  3. Eksekusi Graceful Shutdown di Node.js: Di dalam kode aplikasi, event listener menangkap sinyal SIGTERM, menghentikan http.server.close(), menutup koneksi pool database dengan aman, lalu keluar dengan kode 0.

3. Perbandingan Komparatif / Analisis Pendekatan

Berikut adalah analisis komparatif antara berbagai strategi deployment dan manajemen siklus hidup di ekosistem Kubernetes dan Node.js:

Parameter Komparasi Naive Deployment (Tanpa Konfigurasi) Recreate Strategy Rolling Update (Standard) Rolling Update + Graceful Shutdown + Probes (Best Practice)
Downtime Signifikan (Beberapa detik hingga menit) Pasti terjadi downtime total Minimal, namun masih ada dropped connections Nol (Zero-Downtime)
Resource Overhead Rendah Sangat Rendah Sedang (Membutuhkan headroom kapasitas) Sedang
In-flight Requests Terputus paksa (Dropped) Terputus paksa (Dropped) Sebagian besar terputus Tuntas 100% diproses
Kompleksitas Implementasi Sangat Rendah Rendah Sedang Tinggi (Membutuhkan sinkronisasi kode & YAML)
Latensi Selama Rilis Stabil tapi banyak error 5xx N/A (Layanan mati total) Lonjakan error sesaat Stabil, error rate mendekati 0%

[!IMPORTANT] Pemilihan strategi Rolling Update dengan parameter maxSurge: 1 dan maxUnavailable: 0 adalah kombinasi emas untuk memastikan kapasitas klaster tidak pernah berkurang di bawah jumlah replika yang diinginkan selama proses deployment.


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum mengimplementasikan konfigurasi ini, pastikan komponen berikut telah disiapkan di lingkungan Anda:

  • Kubernetes Cluster: Versi 1.24 atau yang lebih baru (mendukung fitur graceful termination yang lebih matang).
  • Node.js Runtime: Versi 18.x LTS atau 20.x LTS.
  • Container Runtime: Docker atau Containerd dengan dukungan manajemen sinyal POSIX yang baik.
  • Tools: kubectl, helm (opsional), dan akses ke Container Registry (Docker Hub, AWS ECR, atau Google Artifact Registry).

Verifikasi akses cluster Anda dengan perintah:

kubectl cluster-info
kubectl get nodes

5. Panduan Implementasi Langkah demi Langkah

Langkah 1: Implementasi Kode Node.js dengan Graceful Shutdown

Buat file server.js yang mengimplementasikan penanganan HTTP server, koneksi database simulasi, dan penangkapan sinyal SIGTERM secara komprehensif.

'use strict';

const http = require('http');

const PORT = process.env.PORT || 3000;
let isShuttingDown = false;

// Simulasi koneksi database atau resource eksternal
const mockDatabase = {
  isConnected: true,
  async close() {
    console.log('Closing database connections...');
    // Simulasi delay penutupan koneksi pool
    await new Promise(resolve => setTimeout(resolve, 2000));
    this.isConnected = false;
    console.log('Database connections closed successfully.');
  }
};

const server = http.createServer((req, res) => {
  // Jika server sedang dalam proses shutdown, tolak request baru
  if (isShuttingDown) {
    res.writeHead(503, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'error', message: 'Server is shutting down' }));
    return;
  }

  if (req.url === '/healthz') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'healthy', database: mockDatabase.isConnected }));
    return;
  }

  if (req.url === '/readyz') {
    if (mockDatabase.isConnected) {
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ status: 'ready' }));
    } else {
      res.writeHead(503, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ status: 'not ready' }));
    }
    return;
  }

  // Simulasi pemrosesan request berat
  setTimeout(() => {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ message: 'Hello from Node.js Microservice!', timestamp: Date.now() }));
  }, 100);
});

server.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

// --- GRACEFUL SHUTDOWN HANDLERS ---

const handleShutdown = async (signal) => {
  console.log(`Received signal ${signal}. Starting graceful shutdown...`);
  isShuttingDown = true;

  // 1. Berhenti menerima koneksi HTTP baru
  server.close(async () => {
    console.log('HTTP server closed. No longer accepting new connections.');

    try {
      // 2. Tutup resource eksternal (Database, Redis, etc)
      await mockDatabase.close();
      console.log('Graceful shutdown completed successfully.');
      process.exit(0);
    } catch (err) {
      console.error('Error during graceful shutdown:', err);
      process.exit(1);
    }
  });

  // 3. Fallback timeout jika proses terlalu lama (force exit)
  setTimeout(() => {
    console.error('Could not close connections in time, forcefully shutting down');
    process.exit(1);
  }, 25000); // 25 detik (kurang dari terminationGracePeriodSeconds)
};

process.on('SIGTERM', () => handleShutdown('SIGTERM'));
process.on('SIGINT', () => handleShutdown('SIGINT'));

// Penanganan error tak terduga
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  handleShutdown('UNHANDLED_REJECTION');
});

process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception thrown:', error);
  handleShutdown('UNCAUGHT_EXCEPTION');
});

Langkah 2: Pembuatan Dockerfile yang Mendukung Sinyal POSIX

[!WARNING] Jangan pernah menjalankan Node.js secara langsung menggunakan perintah npm start atau node server.js sebagai PID 1 di dalam kontainer tanpa init system (seperti tini), karena npm atau shell pembungkus sering kali mengabaikan sinyal SIGTERM dari Kubernetes, mengakibatkan graceful shutdown gagal total.

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Stage 2: Production
FROM node:20-alpine

# Install tini untuk menangani penerusan sinyal POSIX dengan benar
RUN apk add --no-cache tini

WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY server.js package*.json ./

USER node

EXPOSE 3000

ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]

Langkah 3: Konfigurasi Kubernetes Manifests (Deployment & Service)

Buat file manifes kubernetes dengan nama deployment.yaml yang mencakup Readiness, Liveness, dan Startup Probes secara lengkap.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nodejs-microservice
  labels:
    app: nodejs-microservice
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: nodejs-microservice
  template:
    metadata:
      labels:
        app: nodejs-microservice
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: nodejs-app
        image: your-docker-registry/nodejs-microservice:v1.0.0
        imagePullPolicy: Always
        ports:
        - containerPort: 3000
          name: http
        resources:
          limits:
            cpu: "500m"
            memory: "512Mi"
          requests:
            cpu: "100m"
            memory: "128Mi"
        startupProbe:
          httpGet:
            path: /healthz
            port: http
          initialDelaySeconds: 3
          periodSeconds: 5
          failureThreshold: 6
        readinessProbe:
          httpGet:
            path: /readyz
            port: http
          initialDelaySeconds: 5
          periodSeconds: 10
          timeoutSeconds: 2
          successThreshold: 1
          failureThreshold: 3
        livenessProbe:
          httpGet:
            path: /healthz
            port: http
          initialDelaySeconds: 15
          periodSeconds: 15
          timeoutSeconds: 2
          failureThreshold: 3
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: false
          runAsNonRoot: true
          runAsUser: 1000
---
apiVersion: v1
kind: Service
metadata:
  name: nodejs-microservice-svc
  labels:
    app: nodejs-microservice
spec:
  type: ClusterIP
  ports:
  - port: 80
    targetPort: http
    protocol: TCP
    name: http
  selector:
    app: nodejs-microservice

6. Bedah Kode & Penjelasan Detail Baris Kunci

Mari kita bedah beberapa komponen paling krusial dari konfigurasi dan kode di atas:

  1. server.close() pada Node.js: Baris ini memerintahkan instance HTTP server untuk berhenti membuka socket TCP baru pada port 3000, namun tetap mempertahankan koneksi keep-alive yang sedang aktif hingga request selesai diproses.
  2. maxSurge: 1 dan maxUnavailable: 0: Pengaturan strategi Rolling Update ini memastikan bahwa Kubernetes akan membuat 1 Pod baru terlebih dahulu (maxSurge), dan tidak akan mematikan Pod lama hingga Pod baru tersebut benar-benar siap melayani traffic (maxUnavailable: 0).
  3. **`ENTRYPOINT ["/sbin/tini
🏷️ Tags: #Nginx #High Concurrency #Performance #Sysadmin #Web Server