Health Check #
Monitoring memberitahu kita saat sesuatu sudah rusak. Health check mencegah traffic dikirim ke komponen yang belum siap atau sudah rusak. Keduanya perlu — tetapi health check bekerja di lapisan yang lebih real-time dan lebih operasional: load balancer mengandalkannya untuk routing, Kubernetes mengandalkannya untuk restart otomatis, dan pipeline deployment mengandalkannya untuk menentukan apakah deployment berhasil atau perlu di-rollback. Artikel ini membahas cara mengimplementasikan dan mengotomasi health check yang tepat menggunakan Ansible.
Dua Jenis Health Check yang Berbeda #
Sebelum mengimplementasikan, pahami perbedaan antara dua jenis health check yang sering disalahpahami:
Liveness Check — "Apakah aplikasi masih hidup?"
Tujuan: Deteksi deadlock atau kondisi yang tidak bisa pulih sendiri
Respons saat gagal: Container di-restart
Contoh endpoint: GET /health/live → 200 jika proses berjalan
Yang TIDAK boleh dicek: koneksi database, dependency eksternal
Alasan: Jika database down, kita tidak ingin semua container ikut me-restart
dan memperparah situasi dengan cascading failure
Readiness Check — "Apakah aplikasi siap menerima traffic?"
Tujuan: Cegah traffic ke instance yang belum siap atau sedang overwhelmed
Respons saat gagal: Instance dikeluarkan dari load balancer rotation
Contoh endpoint: GET /health/ready → 200 jika semua dependency siap
Yang BOLEH dicek: koneksi database, cache, dependency kritis
Alasan: Jika database tidak terjangkau, instance memang belum siap melayani
dan seharusnya tidak menerima traffic sampai pulih
Mencampur keduanya dalam satu endpoint adalah sumber bug yang umum — health check yang manggil database akan membuat Kubernetes me-restart semua container saat database mati, yang justru menambah tekanan ke database yang baru pulih. Inilah alasan livenessProbe dan readinessProbe di Kubernetes adalah dua field yang terpisah.
State Diagram: Siklus Health Aplikasi #
Aplikasi berpindah antar state tergantung pada kondisi internal dan dependency-nya. Memahami transisi ini penting untuk mendesain health check yang benar:
stateDiagram-v2
[*] --> Starting: "container start"
Starting --> Healthy: "startup probe OK"
Starting --> Unhealthy: "startup timeout"
Healthy --> Degraded: "dependency down"
Degraded --> Unhealthy: "timeout exceeded"
Degraded --> Healthy: "dependency pulih"
Healthy --> Overloaded: "resource exhaustion"
Overloaded --> Degraded: "load turun"
Unhealthy --> Recovering: "restart / intervention"
Recovering --> Healthy: "liveness OK"
Healthy --> [*]: "graceful shutdown"
Unhealthy --> [*]: "kill / OOM"
Perhatikan bahwa Degraded dan Overloaded adalah state yang berbeda dari Unhealthy. Aplikasi yang degraded masih bisa menjawab beberapa jenis request — mungkin read tapi tidak write, atau request yang tidak butuh cache. readiness check yang memaksa instance keluar dari rotation pada state degraded memastikan traffic dialihkan ke instance yang masih sehat, tanpa me-restart instance yang sebenarnya masih fungsional untuk sebagian beban.
Probe Type: HTTPGet, TCPSocket, dan exec #
Kubernetes mendukung tiga tipe probe, masing-masing untuk kasus yang berbeda. Memilih tipe yang salah adalah sumber umum bug health check:
flowchart TD
A["Butuh health check?"] --> B{"Bisa HTTP?"}
B -- "Ya" --> C["httpGet probe"]
B -- "Tidak" --> D{"Cukup TCP<br/>open?"}
D -- "Ya" --> E["tcpSocket probe"]
D -- "Tidak" --> F["exec probe"]
C --> G{"Tujuan cek apa?"}
E --> G
F --> G
G -- "Aplikasi<br/>responsif" --> H["httpGet"]
G -- "Port terbuka" --> I["tcpSocket"]
G -- "Logika custom" --> J["exec / custom HTTP"]
| Tipe Probe | Yang Dicek | Kapan Dipakai | Contoh |
|---|---|---|---|
httpGet |
Endpoint HTTP return kode 2xx | Aplikasi yang serve HTTP/HTTPS | GET /health/live → 200 |
tcpSocket |
Koneksi TCP ke port bisa dibuka | Aplikasi non-HTTP (database, message queue) | nc -z db-host 5432 |
exec |
Perintah di dalam container return 0 | Validasi yang butuh logic kompleks (cek file, jalankan script) | cat /tmp/ready && exit 0 |
Decision Tree: Pilih Tipe Probe yang Tepat #
flowchart TD
A["Start: butuh probe"] --> B{"Aplikasi serve<br/>HTTP?"}
B -- "Ya" --> C["httpGet probe<br/>ke /health/live atau /health/ready"]
B -- "Tidak" --> D{"Port protokol<br/>cukup info?"}
D -- "Ya" --> E["tcpSocket probe<br/>ke port service"]
D -- "Tidak" --> F["exec probe<br/>jalankan script validasi"]
C --> G{"Perlu cek<br/>logika bisnis?"}
G -- "Ya" --> H["httpGet endpoint custom<br/>return JSON dengan detail"]
G -- "Tidak" --> I["httpGet endpoint sederhana<br/>return 200 saja"]
Anti-Pattern: tcpSocket untuk Web Server vs httpGet #
# ANTI-PATTERN: tcpSocket probe untuk web server
livenessProbe:
tcpSocket:
port: 8080
# ✗ Masalah: tcpSocket hanya cek apakah port terbuka dan bisa di-handshake.
# Web server kita bisa crash di layer application (misal: deadlock thread pool,
# panic di handler, atau memory corruption) TETAPI port masih terbuka
# karena listen() masih jalan di socket. tcpSocket akan return SUCCESS
# padahal aplikasi sebenarnya sudah tidak bisa melayani request.
# BENAR: httpGet probe ke endpoint yang menjamin logika aplikasi berjalan
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 3
# ✓ httpGet memaksa aplikasi memproses request HTTP — jika handler panic,
# jika thread pool exhausted, jika ada deadlock, response tidak akan pernah
# kembali dalam timeoutSeconds, dan probe dihitung gagal.
# Kubernetes tahu aplikasi benar-benar tidak sehat dan boleh di-restart.
Mengimplementasikan Health Endpoint di Aplikasi #
Untuk aplikasi Python/Flask, tambahkan endpoint health check yang memisahkan liveness dan readiness dengan jelas:
# roles/app-health/tasks/main.yml
---
- name: Deploy health check module ke aplikasi
template:
src: health.py.j2
dest: "{{ app_dir }}/health.py"
owner: "{{ app_user }}"
mode: '0644'
notify: Restart aplikasi
# templates/health.py.j2
# Health check endpoints untuk {{ app_name }}
from flask import Blueprint, jsonify
import psycopg2
import redis
import time
health_bp = Blueprint('health', __name__)
# Batas waktu untuk dependency check — readiness harus fast
HEALTH_CHECK_TIMEOUT = 2 # detik
@health_bp.route('/health/live')
def liveness():
"""Liveness: cek proses masih berjalan — tidak cek dependency"""
# ✓ Endpoint ini HARUS cepat dan tidak menyentuh I/O.
# Tujuannya hanya membuktikan proses masih bisa handle request.
return jsonify({
"status": "ok",
"service": "{{ app_name }}",
"version": "{{ app_version }}",
"timestamp": time.time()
}), 200
@health_bp.route('/health/ready')
def readiness():
"""Readiness: cek semua dependency siap — return 503 jika ada yang gagal"""
checks = {}
overall_status = "ok"
failed = []
# Cek database
try:
conn = psycopg2.connect(
"{{ db_url }}",
connect_timeout=HEALTH_CHECK_TIMEOUT
)
conn.close()
checks["database"] = "ok"
except Exception as e:
checks["database"] = f"failed: {str(e)}"
overall_status = "degraded"
failed.append("database")
# Cek Redis
try:
r = redis.from_url("{{ redis_url }}")
r.ping()
checks["redis"] = "ok"
except Exception as e:
checks["redis"] = f"failed: {str(e)}"
overall_status = "degraded"
failed.append("redis")
# Status code 503 untuk degraded — load balancer akan keluarkan dari rotation
status_code = 200 if overall_status == "ok" else 503
return jsonify({
"status": overall_status,
"checks": checks,
"failed": failed,
"timestamp": time.time()
}), status_code
Anti-Pattern: Satu Endpoint untuk Semua vs Endpoint Terpisah #
# ANTI-PATTERN: Satu endpoint yang manggil semua dependency
@app.route('/health')
def health():
# ✗ Endpoint ini dipakai untuk BOTH liveness dan readiness
# Kubernetes liveness probe akan memanggil endpoint ini
db_ok = check_database()
redis_ok = check_redis()
if not db_ok or not redis_ok:
return "unhealthy", 500
return "ok", 200
# Masalah: saat database mati, endpoint return 500.
# Kubernetes liveness probe gagal → Kubernetes RESTART container.
# Semua container restart bersamaan saat database mati,
# lalu coba konek ke database yang baru pulih, semua gagal,
# restart loop terjadi, aplikasi tidak pernah pulih.
# BENAR: Dua endpoint dengan tanggung jawab berbeda
@app.route('/health/live')
def liveness():
# ✓ Tidak cek dependency — selalu 200 selama proses berjalan
return jsonify({"status": "ok"}), 200
@app.route('/health/ready')
def readiness():
# ✓ Cek dependency, return 503 jika ada yang gagal
db_ok = check_database()
redis_ok = check_redis()
if not db_ok or not redis_ok:
return jsonify({"status": "degraded"}), 503
return jsonify({"status": "ok"}), 200
# Keuntungan: database mati → readiness return 503 → instance keluar
# dari load balancer rotation, TAPI Kubernetes TIDAK restart.
# Instance tetap hidup, hemat resource, dan langsung masuk rotation
# begitu database pulih tanpa restart loop.
Health Check Berbasis Ansible untuk Post-Deployment #
Setelah deployment, Ansible perlu memverifikasi bahwa semua komponen sehat sebelum deployment dianggap berhasil. Tanpa verifikasi ini, deployment “berhasil” bisa saja meninggalkan aplikasi yang tidak bisa menerima traffic:
# playbooks/verify-deployment.yml
---
- name: Verifikasi health setelah deployment
hosts: appservers
gather_facts: false
tasks:
- name: Tunggu port aplikasi terbuka
wait_for:
port: "{{ app_port }}"
host: "{{ inventory_hostname }}"
timeout: 60
state: started
- name: Verifikasi liveness endpoint
uri:
url: "http://{{ inventory_hostname }}:{{ app_port }}/health/live"
method: GET
status_code: 200
timeout: 10
register: liveness_result
until: liveness_result.status == 200
retries: 6
delay: 10
- name: Verifikasi readiness endpoint
uri:
url: "http://{{ inventory_hostname }}:{{ app_port }}/health/ready"
method: GET
status_code: 200
timeout: 10
register: readiness_result
until: readiness_result.status == 200
retries: 12
delay: 10
failed_when: readiness_result.status not in [200]
- name: Verifikasi versi yang berjalan sesuai target
assert:
that:
- readiness_result.json.version is defined
- readiness_result.json.version == app_version
fail_msg: >
Versi tidak sesuai!
Diharapkan: {{ app_version }}
Aktual: {{ readiness_result.json.version | default('tidak terdeteksi') }}
- name: Tampilkan ringkasan health check
debug:
msg:
- "✓ Liveness: {{ liveness_result.json.status }}"
- "✓ Readiness: {{ readiness_result.json.status }}"
- "✓ Version: {{ readiness_result.json.version }}"
- "✓ Database: {{ readiness_result.json.checks.database }}"
- "✓ Redis: {{ readiness_result.json.checks.redis }}"
Verifikasi post-deployment ini biasanya digabung dengan rolling update Ansible (serial: 1 atau serial: 25%) sehingga hanya sebagian kecil fleet yang di-update dan diverifikasi pada satu waktu. Jika verifikasi gagal, Ansible berhenti dan deployment dianggap gagal sebelum fleet setengah jadi dalam versi baru.
Konfigurasi Health Check di Load Balancer #
Deploy konfigurasi health check ke HAProxy menggunakan Ansible. HAProxy mendukung health check berbasis HTTP, TCP, dan custom script:
# roles/haproxy/tasks/health-check.yml
---
- name: Deploy konfigurasi HAProxy dengan health check
template:
src: haproxy.cfg.j2
dest: /etc/haproxy/haproxy.cfg
validate: haproxy -c -f %s
notify: Reload HAProxy
{# templates/haproxy.cfg.j2 #}
global
log /dev/log local0
maxconn 50000
defaults
log global
mode http
timeout connect 5s
timeout client 30s
timeout server 30s
frontend http_front
bind *:80
default_backend app_servers
backend app_servers
balance roundrobin
# ✓ Health check ke endpoint readiness, bukan liveness
option httpchk GET /health/ready HTTP/1.1\r\nHost:\ localhost
http-check expect status 200
# Parameter fall/rise yang penting:
# inter 10s: cek setiap 10 detik
# fall 3: 3x gagal berturut-turut = keluar dari rotation
# rise 2: 2x berhasil berturut-turut = kembali ke rotation
# Ini mencegah flapping saat ada satu response gagal sesaat.
{% for host in groups['appservers'] %}
server {{ host }} {{ hostvars[host]['ansible_default_ipv4']['address'] }}:{{ app_port }} \
check inter 10s fall 3 rise 2
{% endfor %}
Konfigurasi fall 3 rise 2 berarti: keluarkan server dari rotation setelah 3 health check gagal berturut-turut, kembalikan setelah 2 health check berhasil. Parameter ini sangat penting untuk stabilitas — tanpa threshold, satu response gagal sesaat (misalnya network blip) bisa mengeluarkan instance dari rotation dan menurunkan kapasitas secara tidak perlu.
Jangan pernah set fall 1 di health check. Single failure = single eviction = satu packet loss = instance keluar dari load balancer. Selalu butuh minimal 2-3 gagal berturut-turut untuk consider unhealthy, dan minimal 1-2 berhasil untuk consider recovered.
Health Check untuk Kubernetes Probe #
Konfigurasi liveness, readiness, dan startup probe di Kubernetes Deployment menggunakan Ansible. Ketiganya punya tujuan yang berbeda dan parameter yang berbeda:
- name: Deploy Deployment dengan probe yang dikonfigurasi dengan benar
kubernetes.core.k8s:
kubeconfig: "{{ k8s_kubeconfig }}"
state: present
definition:
apiVersion: apps/v1
kind: Deployment
metadata:
name: "{{ app_name }}"
namespace: "{{ app_namespace }}"
spec:
template:
spec:
containers:
- name: "{{ app_name }}"
image: "{{ app_image }}:{{ app_version }}"
# ✓ Liveness: hanya cek aplikasi responsif, restart jika hang
livenessProbe:
httpGet:
path: /health/live
port: "{{ app_port }}"
initialDelaySeconds: 15 # Tunggu 15 detik sebelum mulai cek
periodSeconds: 10 # Cek setiap 10 detik
failureThreshold: 3 # Restart setelah 3 kali gagal
timeoutSeconds: 5
# ✓ Readiness: cek dependency, keluar dari traffic jika belum siap
readinessProbe:
httpGet:
path: /health/ready
port: "{{ app_port }}"
initialDelaySeconds: 5 # Lebih cepat dari liveness
periodSeconds: 5
failureThreshold: 3 # Keluarkan dari traffic setelah 3 kali gagal
successThreshold: 1 # Kembalikan setelah 1 kali berhasil
timeoutSeconds: 3
# ✓ Startup: toleransi startup time lama, cegah liveness kill saat init
startupProbe:
httpGet:
path: /health/live
port: "{{ app_port }}"
failureThreshold: 30 # Beri waktu 300 detik (30 x 10s) untuk startup
periodSeconds: 10
Anti-Pattern: Health Check yang Cek Dependency di Liveness #
# ANTI-PATTERN: livenessProbe yang manggil dependency eksternal
livenessProbe:
httpGet:
path: /health/check-all # endpoint yang cek DB, Redis, semua dependency
port: 8080
failureThreshold: 3
# Masalah: saat database mati, livenessProbe gagal, Kubernetes
# kill container, container restart, coba konek DB lagi, gagal,
# kill lagi, restart loop. Database yang harusnya pulih dalam
# 5 menit malah membuat aplikasi tidak tersedia selama 30 menit
# karena thundering herd restart.
# BENAR: livenessProbe yang hanya cek proses, readinessProbe yang cek dependency
livenessProbe:
httpGet:
path: /health/live # endpoint tanpa dependency check
port: 8080
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready # endpoint yang cek DB dan dependency
port: 8080
failureThreshold: 3
# Saat database mati: readinessProbe gagal → pod keluar dari Service endpoint
# (tidak menerima traffic), TAPI pod tetap hidup. Begitu database pulih,
# readinessProbe return 200 → pod kembali menerima traffic. Tidak ada restart.
Alur Kubernetes Probe #
Berikut sequence diagram bagaimana Kubernetes mengkoordinasikan ketiga probe saat pod start dan saat terjadi gangguan:
sequenceDiagram
participant K as Kubelet
participant SP as Startup Probe
participant LP as Liveness Probe
participant RP as Readiness Probe
participant SVC as Service
participant Pod as Application
Note over K,Pod: "Fase Startup"
K->>SP: "Cek pod (setiap 10s)"
SP->>Pod: "GET /health/live"
Pod-->>SP: 200
SP-->>K: Success
K->>LP: "Aktifkan liveness probe"
K->>RP: "Aktifkan readiness probe"
Note over K,Pod: "Fase Normal"
RP->>Pod: "GET /health/ready"
Pod-->>RP: 200
RP-->>K: Ready
K->>SVC: "Tambahkan pod ke endpoint"
SVC->>Pod: "Route traffic"
Note over K,Pod: "Dependency Mati"
RP->>Pod: "GET /health/ready"
Pod-->>RP: "503 (DB down)"
RP-->>K: "Not Ready"
K->>SVC: "Hapus pod dari endpoint"
Note over LP: "Liveness TETAP 200 (proses masih jalan)"
Note over K,Pod: "Aplikasi Hang"
LP->>Pod: "GET /health/live"
Pod-->>LP: "timeout (5s)"
LP->>LP: "failure count++"
LP-->>K: "3x gagal = restart"
K->>Pod: "Kill & restart"
Perhatikan bagaimana startup probe hanya aktif sampai pod pertama kali sukses, setelah itu liveness yang mengambil alih. Ini mencegah skenario klasik: aplikasi butuh 60 detik untuk start (misal: load cache besar, konek ke banyak service), tapi liveness probe di-set 30 detik dengan failure threshold 3 — pod di-kill sebelum sempat start.
Health Check Dashboard dan Alert #
Buat dashboard Grafana yang menampilkan status health semua service. Dashboard ini biasanya menampilkan aggregated health dari beberapa service sekaligus, dengan visual indicator hijau/kuning/merah:
- name: Deploy health check dashboard
copy:
src: files/dashboards/health-overview.json
dest: /var/lib/grafana/dashboards/health-overview.json
owner: grafana
mode: '0640'
notify: Reload Grafana dashboards
Alert rule untuk health check yang gagal:
# roles/prometheus/templates/rules/health-checks.yml.j2
groups:
- name: health_checks
rules:
- alert: ServiceHealthCheckFailing
expr: >
probe_success{job="blackbox"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Health check gagal: {{ '{{' }} $labels.instance {{ '}}' }}"
description: "Endpoint {{ '{{' }} $labels.instance {{ '}}' }} tidak merespons selama 2 menit"
runbook_url: "https://wiki.company.com/runbooks/service-down"
- alert: ServiceHealthDegraded
expr: >
probe_success{job="blackbox"} == 0
for: 30s
labels:
severity: warning
annotations:
summary: "Service degraded: {{ '{{' }} $labels.instance {{ '}}' }}"
description: "Health check intermittent failure detected"
Perhatikan perbedaan for: 2m vs for: 30s — alert critical butuh waktu lebih lama untuk konfirmasi (mengurangi false positive dari blip sesaat), sementara warning lebih sensitif untuk deteksi dini.
Health Check untuk Cron dan Background Job #
Health check tidak hanya untuk HTTP service. Untuk cron job dan background worker, pola yang umum adalah file heartbeat yang di-update setiap kali job berjalan, lalu di-monitor oleh health check service:
# roles/cron-health/tasks/main.yml
---
- name: Setup cron job dengan heartbeat
cron:
name: "data-sync-heartbeat"
minute: "*/5"
job: >
/opt/app/run-sync.sh &&
/usr/bin/date +%s > /var/run/app-sync.heartbeat
user: "{{ app_user }}"
- name: Setup health check service yang monitor heartbeat
template:
src: heartbeat-check.sh.j2
dest: /opt/monitoring/heartbeat-check.sh
mode: '0755'
- name: Cron job untuk health check service
cron:
name: "heartbeat-monitor"
minute: "*/1"
job: "/opt/monitoring/heartbeat-check.sh {{ app_name }} 600"
user: monitoring
{# templates/heartbeat-check.sh.j2 #}
#!/bin/bash
# Monitor heartbeat file dan update exporter metric
SERVICE=$1
MAX_AGE_SECONDS=$2
HEARTBEAT_FILE="/var/run/${SERVICE}.heartbeat"
if [ ! -f "$HEARTBEAT_FILE" ]; then
echo "heartbeat_missing{service=\"$SERVICE\"} 1" | \
curl --data-binary @- http://localhost:9115/metrics/job/heartbeat
exit 0
fi
HEARTBEAT=$(cat "$HEARTBEAT_FILE")
NOW=$(date +%s)
AGE=$((NOW - HEARTBEAT))
if [ $AGE -gt $MAX_AGE_SECONDS ]; then
echo "heartbeat_stale{service=\"$SERVICE\"} 1" | \
curl --data-binary @- http://localhost:9115/metrics/job/heartbeat
else
echo "heartbeat_ok{service=\"$SERVICE\"} 1" | \
curl --data-binary @- http://localhost:9115/metrics/job/heartbeat
fi
Pola heartbeat ini bisa juga dipakai untuk memantau ETL job, scheduled task, atau proses sinkronisasi data yang tidak expose endpoint HTTP.
Kapan Health Check Tidak Cukup #
Health check adalah kontrol operasional real-time. Untuk visibilitas yang lebih dalam (latency, error rate, throughput), kita membutuhkan metrik dan tracing (lihat Metric Collection dan Tracing). Untuk alert yang lebih proaktif sebelum user mengajukan keluhan, kita membutuhkan alerting (lihat Alerting). Health check adalah pondasi yang harus ada dulu — tiga pilar observability saling melengkapi:
flowchart TD
Obs["Observability"] --> Logs["Logs"]
Obs --> Metrics["Metrics"]
Obs --> Tracing["Tracing"]
Logs --> LogsDesc["Apa yang terjadi (event narrative)"]
Metrics --> MetricsDesc["Seberapa sering / seberapa lambat (aggregated number)"]
Tracing --> TracingDesc["Di mana request lambat dalam perjalanan"]
Health check bekerja di level yang berbeda lagi — tidak masuk ke tiga pilar karena ia bukan tentang “apa yang terjadi” tapi “apakah boleh terima traffic lagi”. Ia adalah decision support untuk orchestrator (Kubernetes, load balancer) yang perlu jawaban ya/tidak secara real-time.
Ringkasan #
- Liveness dan readiness adalah dua endpoint yang berbeda dengan tujuan berbeda — jangan gabungkan keduanya dalam satu endpoint.
- Liveness probe tidak boleh cek dependency (database, Redis) — jika dependency down, kita tidak ingin semua container ikut me-restart dan memperparah situasi.
- Readiness probe boleh dan harus cek dependency — instance yang dependency-nya tidak terjangkau memang belum siap melayani traffic.
- Pilih tipe probe yang tepat:
httpGetuntuk web service,tcpSocketuntuk port check sederhana,execuntuk validasi custom yang kompleks.tcpSocketprobe tidak cukup untuk web server — ia hanya cek port, tidak cek apakah handler bisa merespons. GunakanhttpGetke endpoint/health/live.- Parameter
falldanrisedi HAProxy mencegah flapping —fall 3 rise 2lebih stabil daripadafall 1.- startupProbe di Kubernetes untuk aplikasi dengan startup time lama — mencegah liveness probe membunuh aplikasi yang masih dalam proses startup.
- Heartbeat file untuk health check cron job dan background worker yang tidak expose HTTP endpoint.
- Dalam deployment pipeline Ansible, selalu verify deployment dengan health check setelah rolling update sebelum marking deployment sebagai sukses.
- Health check adalah pondasi — untuk visibilitas lebih dalam butuh log, metric, dan tracing (lihat Logging, Metric Collection, Tracing).