Anti-Pattern Networking #

Jaringan (networking) di Kubernetes mengadopsi model yang sangat berbeda dibandingkan dengan lingkungan komparasi tradisional seperti mesin virtual (VM) standalone atau penanganan kontainer Docker standar. Sementara Docker menggunakan model pemetaan port host (port mapping) dengan alokasi NAT dinamis, Kubernetes menerapkan model jaringan datar (flat network) di mana setiap Pod mendapatkan alamat IP unik yang dapat dihubungi langsung dari node mana pun tanpa bantuan translasi alamat.

Kemudahan abstraksi ini sering kali membuat kita lengah. Masalah jaringan di Kubernetes sangat rentan terhadap kegagalan senyap (silent failure)—layanan tampak berfungsi normal selama tahap pengujian lokal atau staging dengan beban rendah, tetapi akan mengalami kegagalan sistematis saat dihadapkan pada lalu lintas produksi yang berfluktuasi tinggi. Artikel ini merangkum delapan anti-pattern jaringan yang paling sering ditemui di lingkungan produksi beserta konsekuensi teknis dan solusi alternatifnya yang lebih kokoh.


1. Hindari Hardcode IP Pod untuk Komunikasi Antar-Layanan #

IP Pod di Kubernetes bersifat ephemeral atau sementara. Setiap kali sebuah Pod mengalami restart akibat kegagalan aplikasi, proses rolling update, atau ketika node worker dipindahkan (drained) untuk pemeliharaan, Pod tersebut akan dihancurkan dan dibuat ulang dengan alamat IP baru dari subnet CIDR node.

Konsekuensi Teknis #

Jika kita mengonfigurasi mikroservis (misalnya, order-service) untuk menghubungi database PostgreSQL dengan mendaftarkan IP fisik Pod database secara langsung di berkas konfigurasi, maka konektivitas akan terputus seketika saat Pod database tersebut dijadwalkan ulang. Aplikasi kita akan memunculkan galat connection refused atau timeout, dan kita dipaksa melakukan pembaruan konfigurasi serta deployment ulang secara manual di tengah malam.

Perbandingan Implementasi #

# ANTI-PATTERN: Mendaftarkan IP Pod database secara langsung di kode aplikasi
# Jika Pod database restart, alamat IP 10.244.2.34 tidak akan lagi valid.
DATABASE_HOST = "10.244.2.34"
DATABASE_PORT = 5432

# BENAR: Gunakan nama domain Service Kubernetes yang stabil
# Nama domain ini diresolusi secara dinamis ke IP ClusterIP yang konstan oleh CoreDNS.
DATABASE_HOST = "postgres-db.production.svc.cluster.local"
DATABASE_PORT = 5432

2. Hindari Membuat Satu LoadBalancer Service untuk Setiap Mikroservis #

Ketika kita perlu mengekspos aplikasi internal ke internet luar, Kubernetes menyediakan tipe Service bernama LoadBalancer. Namun, membuat objek Service dengan spesifikasi ini untuk setiap mikroservis adalah pendekatan arsitektur yang tidak efisien.

Konsekuensi Teknis #

Di lingkungan cloud publik (seperti AWS, GCP, atau Azure), setiap kali API Server mendeteksi Service bertipe LoadBalancer, ia akan meminta API cloud provider untuk menginstansiasi satu unit Load Balancer fisik baru (misalnya AWS Network Load Balancer atau GCP Layer 4 Load Balancer). Jika kluster kita memiliki 30 mikroservis internal yang semuanya diekspos dengan cara ini:

  1. Biaya Membengkak: Cloud provider mengenakan biaya per jam untuk setiap instans load balancer fisik yang dibuat.
  2. Manajemen SSL/TLS Rumit: Kita harus mengonfigurasi sertifikat SSL/TLS secara terpisah di setiap load balancer.
  3. Keterbatasan IP Publik: Kita akan menghabiskan kuota IP publik VPC kita secara tidak perlu.

Perbandingan Implementasi #

# ANTI-PATTERN: Mengekspos setiap mikroservis dengan LoadBalancer terpisah
# ✗ Tindakan ini membuat load balancer fisik baru untuk setiap manifes.
apiVersion: v1
kind: Service
metadata:
  name: billing-service
spec:
  type: LoadBalancer
  ports:
  - port: 80
  selector:
    app: billing
---
# BENAR: Gunakan tipe Service ClusterIP dan kelola rute luar menggunakan objek Ingress tunggal
# ✓ Hanya satu Load Balancer fisik yang dibuat (di depan Ingress Controller) untuk membagi lalu lintas.
apiVersion: v1
kind: Service
metadata:
  name: billing-service
spec:
  type: ClusterIP  # Hanya dapat diakses dari dalam kluster
  ports:
  - port: 80
  selector:
    app: billing
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: main-ingress
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - api.example.com
    secretName: api-tls-secret
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /billing
        pathType: Prefix
        backend:
          service:
            name: billing-service
            port:
              number: 80

3. Hindari Membiarkan Pod Masuk ke Endpoints Tanpa Readiness Probe #

Secara default, jika kita tidak menentukan konfigurasi readinessProbe di manifes Deployment, Kubernetes akan menganggap kontainer aplikasi siap menerima lalu lintas masuk segera setelah status kontainer tersebut tercatat sebagai Running.

Konsekuensi Teknis #

Selama proses deployment baru (rolling update), Kubernetes akan membuat Pod versi baru dan langsung memasukkannya ke dalam daftar Endpoints Service begitu proses kontainernya dimulai. Padahal, aplikasi web modern sering kali membutuhkan waktu (antara 5 hingga 30 detik) untuk melakukan inisialisasi framework, memuat konfigurasi dari secret manager, atau membuka koneksi pool ke database.

Akibatnya, lalu lintas kueri HTTP dari pengguna akan dialihkan ke Pod baru yang belum siap memproses permintaan. Pengguna kita akan mendapatkan respons galat 502 Bad Gateway atau 503 Service Unavailable di setiap siklus rilis aplikasi.

Perbandingan Implementasi #

# ANTI-PATTERN: Deployment tanpa definisi pemeriksaan kesiapan (readiness)
# ✗ Pod langsung menerima traffic begitu proses OS kontainer berjalan.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app-unsafe
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: app
        image: my-app:v2.0.0
        ports:
        - containerPort: 8080
---
# BENAR: Gunakan readinessProbe untuk memverifikasi aplikasi sudah siap memproses kueri
# ✓ Pod hanya akan dimasukkan ke Endpoints Service setelah endpoint /healthz/ready merespons dengan HTTP 200.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app-safe
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: app
        image: my-app:v2.0.0
        ports:
        - containerPort: 8080
        readinessProbe:
          httpGet:
            path: /healthz/ready
            port: 8080
          initialDelaySeconds: 5  # Tunggu 5 detik sebelum mulai memantau
          periodSeconds: 5        # Periksa setiap 5 detik
          failureThreshold: 3     # Keluarkan dari Endpoints jika 3 kali berturut-turut gagal

4. Hindari Penggunaan NodePort untuk Ekspos Publik Lingkungan Produksi #

Service bertipe NodePort adalah cara termudah bagi pemula untuk mengakses aplikasi kluster dari luar, karena ia akan membuka satu port statis bernilai tinggi (antara rentang default 30000–32767) di semua node worker kluster.

Konsekuensi Teknis #

Meskipun fungsional, menggunakan NodePort untuk lingkungan produksi yang menghadap langsung ke internet umum adalah sebuah anti-pattern yang membahayakan keamanan kluster kita:

  1. Celah Keamanan Terbuka Lebar: Port jaringan tinggi terbuka secara langsung di setiap IP publik node worker kita.
  2. Ketiadaan Fitur TLS/SSL Bawaan: NodePort tidak menyediakan enkripsi TLS otomatis tingkat gerbang.
  3. Keterikatan IP Node (Tinggi Kerentanan): Pengguna luar harus mengetahui alamat IP fisik node worker untuk terhubung. Jika node tersebut mati, koneksi pengguna akan terputus kecuali mereka mengganti target koneksi ke IP node worker lain secara manual.

Perbandingan Penanganan Rute Publik #

ANTI-PATTERN (Ekspos NodePort Publik):
  [User] ---> (Koneksi Langsung ke Port 31080) ---> [IP-Node-Worker-1:31080] 
  (Risiko: Port publik terbuka di semua node, tidak ada SSL terpusat)

BENAR (Ekspos Ingress / LoadBalancer Terproteksi):
  [User] ---> (Port 443 HTTPS) ---> [Cloud Load Balancer] ---> [Ingress Controller] ---> [ClusterIP Service]
  (Keamanan: Enkripsi TLS terpusat di Ingress, IP Node tersembunyi di dalam VPC privat)

NodePort hanya boleh digunakan sebagai jembatan internal di belakang sistem Load Balancer eksternal (terutama pada kluster on-premise) atau untuk keperluan uji coba lokal cepat di komputer pengembangan kita.


5. Hindari Menerapkan Default-Deny NetworkPolicy Tanpa Membuka Egress DNS #

Menerapkan kebijakan keamanan zero-trust dengan membuat NetworkPolicy global berjenis default-deny (baik ingress maupun egress) adalah langkah mitigasi keamanan yang sangat baik untuk mengisolasi lalu lintas antar namespace. Namun, kesalahan fatal sering terjadi saat kita lupa membuka jalur keluar untuk resolusi nama domain (DNS).

Konsekuensi Teknis #

Ketika aturan default-deny memblokir seluruh lalu lintas keluar (egress) dari sebuah namespace, kueri DNS dari Pod aplikasi ke port 53 UDP/TCP CoreDNS di namespace kube-system juga akan di-drop secara senyap oleh sistem firewall kernel Node.

Akibatnya, aplikasi tidak akan mampu melakukan resolusi nama domain eksternal (seperti API pembayaran pihak ketiga) maupun nama domain internal kluster. Aplikasi kita akan terhenti total dengan memunculkan pesan kesalahan unknown host atau connection timeout yang membingungkan.

Perbandingan Implementasi #

# ANTI-PATTERN: Default-deny egress total tanpa pengecualian
# ✗ Semua kueri nama domain akan diblokir total, membuat aplikasi lumpuh.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: block-all-egress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Egress
---
# BENAR: Terapkan default-deny tetapi sertakan izin egress port 53 ke CoreDNS
# ✓ Aplikasi tetap aman dari kebocoran lalu lintas luar, tetapi resolusi nama domain tetap berjalan.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-with-dns
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  # WAJIB: Izinkan kueri DNS internal ke namespace kube-system di port 53
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
  # Tambahkan aturan egress spesifik lain di bawah (misalnya ke database)
  - to:
    - podSelector:
        matchLabels:
          app: postgres-db
    ports:
    - protocol: TCP
      port: 5432

6. Hindari externalTrafficPolicy: Cluster Jika Kita Membutuhkan IP Klien Asli #

Secara default, Service bertipe LoadBalancer atau NodePort menggunakan spesifikasi externalTrafficPolicy: Cluster. Saat paket data dari pengguna luar masuk ke sebuah Node Worker A, kube-proxy akan menerima paket tersebut dan merutenya secara acak ke salah satu Pod backend, yang mungkin berada di Node Worker B.

Konsekuensi Teknis #

Untuk memindahkan paket lintas node ini, Node Worker A harus melakukan proses Translasi Alamat Sumber (Source Network Address Translation / SNAT). Akibatnya, alamat IP sumber pada paket data diubah menjadi alamat IP internal dari Node Worker A.

Ketika paket sampai di Pod aplikasi, aplikasi kita akan melihat bahwa semua lalu lintas kueri masuk berasal dari IP node kluster, bukan IP publik asli dari pengguna. Ini memicu beberapa kerugian krusial:

  1. Gagal Analisis Log: Kita kehilangan jejak IP asli untuk kebutuhan audit keamanan atau penanganan forensik insiden.
  2. Geo-Blocking Tidak Berfungsi: Aplikasi tidak bisa mendeteksi asal negara klien untuk pemfilteran konten.
  3. Keamanan Limitasi Gagal: Sistem pembatasan laju kueri (rate limiting) berbasis IP klien tidak dapat berjalan dengan benar karena semua pengguna dianggap memiliki IP yang sama.

Aliran Perutean Paket dan Modifikasi IP Sumber #

Mari kita pelajari visualisasi perutean paket di bawah ini untuk melihat bagaimana IP asli klien diubah oleh aturan SNAT pada kebijakan default Cluster:

flowchart TD
    subgraph "externalTrafficPolicy: Cluster (Default)"
        direction TB
        ClientA["Klien (IP Asli: 203.0.113.5)"] -->|Paket| LBA["Load Balancer Cloud"]
        LBA --> NodeA["Node Worker A (Tanpa Pod)"]
        NodeA -->|"SNAT (IP Sumber diubah ke IP Node A: 10.0.1.10)"| NodeB["Node Worker B (Ada Pod)"]
        NodeB --> PodB["Pod Backend (Melihat IP Pengirim: 10.0.1.10)"]
    end

    subgraph "externalTrafficPolicy: Local"
        direction TB
        ClientB["Klien (IP Asli: 203.0.113.5)"] -->|Paket| LBB["Load Balancer Cloud"]
        LBB -->|Arahkan hanya ke node dengan Pod| NodeD["Node Worker D (Ada Pod)"]
        NodeD -->|Tanpa SNAT| PodD["Pod Backend (Melihat IP Asli: 203.0.113.5)"]
    end

Perbandingan Konfigurasi Service #

# ANTI-PATTERN: Menggunakan nilai default 'Cluster' saat butuh verifikasi IP Klien
# ✗ Kube-proxy melakukan SNAT silang node, menghilangkan data IP asli pengirim.
apiVersion: v1
kind: Service
metadata:
  name: app-service-no-ip
spec:
  type: LoadBalancer
  externalTrafficPolicy: Cluster  # Nilai bawaan
  ports:
  - port: 80
  selector:
    app: web
---
# BENAR: Atur externalTrafficPolicy ke 'Local'
# ✓ Paket dari Load Balancer hanya dikirim ke node yang memuat Pod backend aktif tanpa manipulasi SNAT.
apiVersion: v1
kind: Service
metadata:
  name: app-service-preserve-ip
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local  # Mempertahankan IP Klien asli
  ports:
  - port: 80
  selector:
    app: web

[!WARNING] Menyetel externalTrafficPolicy: Local dapat menyebabkan pembagian beban (load distribution) menjadi tidak merata jika jumlah replika Pod tidak tersebar rata di setiap Node Worker, karena Load Balancer cloud hanya akan mengirimkan lalu lintas ke Node Worker yang memiliki Pod target aktif.


7. Hindari Mengabaikan Penggunaan Port Logis Berlabel Nama yang Konsisten #

Menuliskan konfigurasi port menggunakan angka mentah (raw integer port numbers) secara berulang di manifes Pod, Service, dan Ingress tanpa memberinya label nama logis (named ports) adalah salah satu anti-pattern manajemen konfigurasi yang paling sering memicu inkonsistensi port mapping.

Konsekuensi Teknis #

Ketika pengembang backend mengubah port listening aplikasi di dalam kode dari port 8080 ke 9000 (misalnya karena perubahan kerangka kerja runtime kontainer), kita harus mencari dan mengganti seluruh angka port tersebut di puluhan baris manifes YAML yang berbeda.

Jika ada satu manifes Service yang terlewat untuk diselaraskan port targetnya, kita akan menghadapi kondisi galat perutean upstream (502 Bad Gateway) di lingkungan produksi. Selain itu, ketiadaan nama port yang terstandarisasi mempersulit integrasi alat eksternal seperti Istio Service Mesh atau sistem monitoring Prometheus untuk mendeteksi port scraping secara dinamis.

Perbandingan Penulisan Port Manifes #

# ANTI-PATTERN: Menggunakan angka port mentah yang tersebar di banyak manifes
# ✗ Sangat rentan terhadap kesalahan penulisan jika port backend aplikasi berubah.
apiVersion: v1
kind: Pod
metadata:
  name: web-app-raw-port
spec:
  containers:
  - name: app
    image: my-app:v1
    ports:
    - containerPort: 8080  # Angka port mentah
---
apiVersion: v1
kind: Service
metadata:
  name: app-service-raw
spec:
  ports:
  - port: 80
    targetPort: 8080  # Harus diselaraskan manual dengan containerPort di atas
  selector:
    app: web
---
# BENAR: Definisikan nama port (Named Port) di tingkat spesifikasi kontainer Pod
# ✓ Service dan Ingress cukup merujuk pada nama label tersebut, terbebas dari keterikatan angka port fisik.
apiVersion: v1
kind: Pod
metadata:
  name: web-app-named-port
  labels:
    app: web
spec:
  containers:
  - name: app
    image: my-app:v1
    ports:
    - name: http-web-port  # Definisikan nama port di sini
      containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: app-service-named
spec:
  ports:
  - port: 80
    targetPort: http-web-port  # Rujuk nama port secara logis
  selector:
    app: web

8. Hindari Kebocoran Kueri DNS ke Luar Akibat Konfigurasi ndots yang Terlalu Besar #

Secara default, setiap Pod di Kubernetes dikonfigurasi dengan nilai parameter ndots:5 di dalam berkas resolusi DNS /etc/resolv.conf-nya.

Konsekuensi Teknis #

Parameter ndots:5 menginstruksikan sistem resolver internal kontainer bahwa jika nama domain yang dicari memiliki kurang dari 5 karakter titik (.), maka resolver harus mencari nama domain tersebut dengan menempelkan akhiran nama domain pencarian internal (search domains) kluster terlebih dahulu, sebelum mencoba mencarinya sebagai domain absolut eksternal.

Jika aplikasi kita sering melakukan panggilan HTTP ke domain eksternal seperti api.stripe.com (yang hanya memiliki 2 titik):

  1. Resolver mencoba mencari api.stripe.com.production.svc.cluster.local (Gagal/NXDOMAIN).
  2. Resolver mencoba mencari api.stripe.com.svc.cluster.local (Gagal/NXDOMAIN).
  3. Resolver mencoba mencari api.stripe.com.cluster.local (Gagal/NXDOMAIN).
  4. Akhirnya resolver mencoba mencari api.stripe.com secara absolut (Berhasil).

Untuk setiap panggilan kueri ke luar, resolver kita melakukan 3 kueri DNS sia-sia ke CoreDNS. Di bawah beban produksi yang tinggi, hal ini memicu banjir lalu lintas kueri DNS (DNS query overload) yang dapat membuat CPU CoreDNS melonjak hingga 100% dan memperlambat latensi seluruh aplikasi kluster kita.

Perbandingan Penanganan Kueri Eksternal #

# ANTI-PATTERN: Memanggil kueri domain eksternal standar tanpa tanda titik akhir
# ✗ Ini memicu CoreDNS melakukan 3 kueri internal sia-sia terlebih dahulu akibat aturan ndots.
response = requests.get("https://api.stripe.com/v3/charges")

# BENAR: Gunakan tanda titik penutup (FQDN Trailing Dot) pada domain eksternal
# ✓ Titik di akhir memberi tahu resolver bahwa domain ini bersifat absolut, melewati kueri pencarian internal.
response = requests.get("https://api.stripe.com./v3/charges")

Kita juga dapat memperkecil nilai ndots secara terpusat pada manifes spesifikasi Pod jika aplikasi kita tidak pernah menggunakan panggilan domain pendek lintas namespace:

# Alternatif mitigasi: Perkecil nilai ndots di manifes Pod
spec:
  dnsConfig:
    options:
      - name: ndots
        value: "1"  # DNS hanya akan menempelkan search path jika domain tidak memiliki titik sama sekali

Checklist Review Jaringan Kluster #

Gunakan lembar kerja di bawah ini untuk mengaudit konfigurasi jaringan manifes kita sebelum melakukan rilis ke lingkungan produksi:

ARAH RUTE & STRUKTUR SERVICE:
  □ Semua Pod aplikasi wajib terproteksi oleh minimal satu readinessProbe
  □ Seluruh Service internal menggunakan tipe ClusterIP, bukan NodePort/LoadBalancer
  □ Pemetaan port di manifes menggunakan nama logis (named ports) yang konsisten
  □ Nama domain eksternal di dalam kode aplikasi ditulis dengan tanda titik di akhir (trailing dot)

KEAMANAN & ISOLASI (NETWORK_POLICY):
  □ Namespace dilindungi kebijakan default-deny untuk meminimalkan jalur serangan lateral
  □ Setiap NetworkPolicy default-deny wajib memuat izin port 53 UDP/TCP ke namespace kube-system
  □ IP CIDR database eksternal terproteksi dengan aturan egress yang presisi

PRESERVASI IDENTITAS (SOURCE_IP):
  □ Service LoadBalancer penampung log lalu lintas publik menggunakan externalTrafficPolicy: Local
  □ Konfigurasi annotations Ingress Controller diselaraskan dengan parameter header X-Forwarded-For

Ringkasan #

  • Abstraksikan alamat IP Pod: Hindari penulisan alamat IP Pod secara statis di dalam kode aplikasi. Selalu gunakan nama DNS Service internal kluster yang dijamin stabil dan dinamis oleh CoreDNS.
  • Hemat biaya Load Balancer: Gunakan satu gerbang Ingress Controller tunggal untuk membagi lalu lintas ke puluhan Service ClusterIP internal kluster daripada memboroskan biaya cloud untuk membuat LoadBalancer Service terpisah bagi setiap aplikasi.
  • Cegah kegagalan rilis dengan readinessProbe: Pastikan seluruh kontainer memiliki readinessProbe agar Kubernetes tidak tergesa-gesa mengirimkan lalu lintas ke Pod baru yang masih dalam proses inisialisasi internal.
  • Lindungi node dengan mematikan NodePort: Jangan mengekspos NodePort langsung ke internet umum. Pertahankan rute terpusat melalui Ingress Controller demi alasan keamanan dan fleksibilitas TLS termination.
  • Pelihara jalur kueri DNS CoreDNS: Saat merancang arsitektur zero-trust menggunakan NetworkPolicy, pastikan port 53 UDP/TCP ke namespace CoreDNS dikecualikan agar aplikasi tidak kehilangan kemampuan resolusi nama domain.
  • Pertahankan IP Klien Asli secara sadar: Gunakan parameter externalTrafficPolicy: Local pada LoadBalancer Service untuk menghindari manipulasi SNAT oleh kube-proxy, sehingga aplikasi kita dapat memproses IP asli dari pengguna luar.
  • Kurangi beban CoreDNS dengan trailing dot: Tambahkan tanda titik penutup pada pemanggilan alamat domain eksternal di aplikasi kita untuk menghindari pemborosan kueri pencarian internal kluster akibat aturan default ndots:5.

← Sebelumnya: Network Troubleshooting   Berikutnya: ConfigMap →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact