Tampilan
Kenapa penting
Manifest di bab sebelumnya sudah bisa menjalankan order-service, tetapi belum siap production: URL database ditanam di image, password tidak punya tempat yang benar, Kubernetes tidak tahu kapan aplikasi siap menerima trafik, dan JVM bisa mati mendadak karena memori. Banyak insiden production di tim Java bukan karena bug logika, melainkan karena probe yang salah arah, OOMKilled, atau request hilang saat rilis. Bab ini membahas "lima baris YAML" yang membedakan deployment coba-coba dengan deployment yang bisa diandalkan — dan itu termasuk hal yang sering ditanyakan saat code review maupun interview.
Konsep inti
ConfigMap: konfigurasi di luar image
Prinsip 12-factor: satu image untuk semua environment, konfigurasi disuntikkan dari luar. ConfigMap menyimpan konfigurasi non-rahasia sebagai pasangan kunci–nilai atau isi file.
yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: order-service-config
namespace: shop
data:
SPRING_PROFILES_ACTIVE: "k8s"
SPRING_DATASOURCE_URL: "jdbc:postgresql://postgres:5432/orders"
application-k8s.yaml: |
app:
payment:
base-url: http://payment-service
timeout: 2sAda dua cara memakainya:
| Cara | Contoh | Kelebihan | Kekurangan |
|---|---|---|---|
| Env var | envFrom / valueFrom | Sederhana; Spring membaca otomatis | Perubahan baru terbaca setelah Pod di-restart |
| Volume | file di /config/application-k8s.yaml | Cocok untuk file konfigurasi utuh | Perlu memberi tahu Spring lokasi file-nya |
Spring Boot memetakan env var SPRING_DATASOURCE_URL ke properti spring.datasource.url (relaxed binding), jadi kode aplikasi tidak perlu berubah. File yang di-mount bisa dimuat dengan SPRING_CONFIG_IMPORT=optional:file:/config/.
Secret: untuk nilai rahasia — tapi bukan enkripsi
Secret strukturnya mirip ConfigMap, tetapi ditujukan untuk password, token, dan kunci API. Kubernetes memperlakukannya sedikit berbeda (tidak ditampilkan di describe, bisa dibatasi RBAC terpisah, disimpan di tmpfs saat di-mount).
yaml
apiVersion: v1
kind: Secret
metadata:
name: order-service-db
namespace: shop
type: Opaque
stringData: # ditulis plain, disimpan sebagai base64 di field data
SPRING_DATASOURCE_USERNAME: orders_app
SPRING_DATASOURCE_PASSWORD: ganti-di-cluster-nyataSecret hanya base64
Nilai di data hanya di-encode base64 — echo b3JkZXJzX2FwcA== | base64 -d langsung membukanya. Lindungi dengan RBAC (siapa boleh get secret), aktifkan enkripsi etcd at rest di cluster, dan jangan pernah commit manifest Secret berisi nilai asli ke Git.
Untuk alur GitOps ada dua pendekatan populer: Sealed Secrets (kamu commit SealedSecret terenkripsi yang hanya bisa dibuka controller di cluster) dan External Secrets Operator (manifest hanya berisi referensi; nilai ditarik dari Vault atau secret manager cloud). Pilihan itu biasanya diputuskan tim platform; tugasmu memastikan aplikasi membaca rahasia dari env/file, bukan dari kode.
Probe: liveness, readiness, startup
kubelet memeriksa kesehatan container lewat tiga jenis probe dengan konsekuensi berbeda:
| Probe | Pertanyaan | Bila gagal | Endpoint Actuator |
|---|---|---|---|
startupProbe | Sudah selesai start? | Ditunggu sampai batas, lalu container di-restart | /actuator/health/liveness |
livenessProbe | Proses masih hidup / tidak macet? | Container di-restart | /actuator/health/liveness |
readinessProbe | Siap menerima trafik? | Pod dikeluarkan dari endpoint Service (tidak di-restart) | /actuator/health/readiness |
Spring Boot Actuator menyediakan health group liveness dan readiness yang dirancang untuk ini. Aktifkan secara eksplisit agar perilakunya sama di laptop dan cluster:
yaml
# application.yaml — potongan
management:
endpoint:
health:
probes:
enabled: true
endpoints:
web:
exposure:
include: health,info,prometheusAturan emasnya: liveness jangan bergantung pada sistem eksternal. Bila database down dan liveness ikut gagal, Kubernetes me-restart semua Pod bersamaan — masalah kecil berubah menjadi outage. Startup probe melindungi aplikasi Spring yang start-nya lambat: selama startup probe belum lulus, liveness belum dijalankan.
Requests, limits, dan JVM
- requests — jumlah yang "dipesan"; dipakai scheduler memilih node.
- limits — batas atas. Melewati limit memori → container dibunuh (
OOMKilled, exit code 137). Melewati limit CPU → di-throttle (melambat), tidak dibunuh.
JVM 25 membaca limit cgroup, tetapi default MaxRAMPercentage hanya 25% dari memori yang terlihat. Dengan limit 1 GiB, heap maksimum hanya sekitar 256 MiB — boros. Sebaliknya, heap 100% juga salah karena JVM butuh memori non-heap (metaspace, thread stack, code cache, direct buffer). Untuk container yang hanya berisi satu JVM, 70–75% adalah titik awal yang umum:
yaml
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"-XX:+ExitOnOutOfMemoryError membuat JVM keluar saat OutOfMemoryError, sehingga Kubernetes me-restart container daripada membiarkannya setengah hidup.
QoS class
Dari requests dan limits, Kubernetes memberi setiap Pod QoS class yang menentukan urutan korban saat node kehabisan memori:
| QoS | Syarat | Saat node kekurangan memori |
|---|---|---|
Guaranteed | Semua container: requests = limits untuk CPU dan memori | Dikorbankan paling akhir |
Burstable | Ada requests/limits, tapi tidak memenuhi Guaranteed | Di tengah |
BestEffort | Tidak ada requests maupun limits sama sekali | Dikorbankan pertama |
Praktik umum untuk service Java: memori requests = limits (perilaku JVM jadi bisa diprediksi), CPU requests diisi, sedangkan limit CPU dibuat longgar atau mengikuti kebijakan tim — throttling CPU membuat start Spring dan GC terasa lambat.
Graceful shutdown
Saat Pod dihapus (rilis, scale down, node dikosongkan), dua hal terjadi paralel: Pod dikeluarkan dari endpoint Service, dan container menerima SIGTERM. Bila aplikasi langsung berhenti, request yang masih dalam perjalanan gagal. Polanya:
preStopjeda beberapa detik agar load balancer berhenti mengirim trafik baru.- Spring Boot menerima
SIGTERM, berhenti menerima koneksi baru, lalu menunggu request berjalan selesai (graceful shutdown aktif secara default sejak Boot 3.4, batas waktunyaspring.lifecycle.timeout-per-shutdown-phase, default 30 detik). - Bila melewati
terminationGracePeriodSeconds(default 30), container di-SIGKILL. Jadi grace period harus > jedapreStop+ waktu shutdown aplikasi.
Image pull policy dan tag tetap
imagePullPolicy default IfNotPresent, kecuali tag latest atau tanpa tag — saat itu default menjadi Always. Gunakan tag yang tidak pernah ditimpa (versi semantik atau SHA commit, mis. order-service:1.4.0 atau order-service:git-3f9c2ab), atau lebih ketat lagi pin dengan digest @sha256:.... Dengan tag tetap, setiap Pod pasti menjalankan kode yang sama dan rollback benar-benar kembali ke versi lama.
Contoh di dunia backend
Deployment order-service dari bab sebelumnya, kini lengkap dengan konfigurasi, rahasia, probe, sumber daya, dan graceful shutdown:
yaml
# Potongan — ConfigMap & Secret seperti di atas sudah di-apply
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: shop
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
terminationGracePeriodSeconds: 45
containers:
- name: app
image: registry.example.com/shop/order-service:1.4.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
envFrom:
- configMapRef:
name: order-service-config
- secretRef:
name: order-service-db
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
- name: SPRING_CONFIG_IMPORT
value: "optional:file:/config/"
volumeMounts:
- name: app-config
mountPath: /config
readOnly: true
resources:
requests:
cpu: 500m
memory: 768Mi
limits:
memory: 768Mi
startupProbe:
httpGet:
path: /actuator/health/liveness
port: http
periodSeconds: 5
failureThreshold: 24 # maks. 2 menit untuk start
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: http
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: http
periodSeconds: 5
failureThreshold: 3
lifecycle:
preStop:
sleep:
seconds: 10 # aksi sleep bawaan Kubernetes modern; image tidak perlu punya `sleep`
volumes:
- name: app-config
configMap:
name: order-service-config
items:
- key: application-k8s.yaml
path: application-k8s.yamlHitungan grace period: 10 detik preStop + maksimal 30 detik graceful shutdown Spring = 40 detik, masih di bawah 45. Pod ini berkelas Burstable (ada limit memori, tanpa limit CPU). Untuk memeriksa hasilnya:
bash
kubectl apply -f k8s/
kubectl get pod -n shop -l app=order-service -o jsonpath='{.items[0].status.qosClass}'
kubectl describe pod -n shop -l app=order-service | grep -A3 "Last State" # cari OOMKilled
kubectl port-forward deploy/order-service 8080:8080 -n shop
curl http://localhost:8080/actuator/health/readinessPertanyaan interview
1. Apa beda liveness dan readiness probe, dan kenapa liveness tidak boleh mengecek database?
Liveness yang gagal membuat kubelet me-restart container; readiness yang gagal hanya mengeluarkan Pod dari endpoint Service. Bila database down, me-restart aplikasi tidak memperbaiki apa pun dan justru membuat semua replika restart bersamaan (cascading failure). Gangguan dependensi cukup tercermin di readiness atau ditangani dengan circuit breaker.
2. Kenapa service Java bisa OOMKilled padahal heap belum penuh?
Limit memori container berlaku untuk seluruh proses, bukan hanya heap. JVM juga memakai metaspace, thread stack, code cache, GC structures, dan direct buffer (mis. dari Netty). Bila MaxRAMPercentage atau -Xmx terlalu dekat dengan limit, total memori melewati limit dan kernel membunuh proses. Sisakan ruang 25–30% untuk non-heap dan pantau RSS container.
3. Bagaimana cara mencegah request gagal saat rolling update?
Pastikan readiness probe benar agar Pod baru hanya menerima trafik setelah siap, gunakan maxUnavailable: 0, tambahkan jeda preStop agar endpoint sempat dihapus sebelum aplikasi berhenti, aktifkan graceful shutdown Spring Boot, dan set terminationGracePeriodSeconds lebih besar dari total jeda dan waktu shutdown.
Jebakan umum
- Liveness diarahkan ke
/actuator/healthgabungan, sehingga gangguan database memicu restart massal. - Tanpa startup probe, liveness membunuh aplikasi Spring yang start-nya lama sebelum sempat siap —
CrashLoopBackOffpadahal tidak ada bug. -XmxatauMaxRAMPercentagemendekati 100% limit memori →OOMKilled(exit code 137) acak di bawah beban.- Menganggap Secret terenkripsi lalu meng-commit manifest berisi password asli ke Git.
- Mengubah ConfigMap lalu heran aplikasi tidak berubah — env var hanya dibaca saat start; jalankan
kubectl rollout restart deploy/order-service. terminationGracePeriodSecondslebih pendek dari jedapreStop+ waktu shutdown, sehingga container di-SIGKILLdi tengah request.
Ringkasan
- Satu image untuk semua environment: konfigurasi lewat ConfigMap, rahasia lewat Secret, disuntikkan sebagai env var atau file.
- Secret hanya base64 — andalkan RBAC, enkripsi etcd, dan Sealed Secrets / External Secrets untuk GitOps.
- Startup dan liveness ke
/actuator/health/liveness, readiness ke/actuator/health/readiness; liveness tidak mengecek dependensi. - Requests untuk penjadwalan, limits sebagai batas; setel
MaxRAMPercentagesekitar 70–75% dan pahami QoS class. - Graceful shutdown =
preStopjeda + shutdown Spring +terminationGracePeriodSecondsyang cukup; selalu pakai tag image tetap.