Di Kubernetes, terdapat Ingress yang dapat dipakai untuk mempublikasikan beberapa Service berbeda melalui satu IP yang sama. Akan tetapi, sintaks Ingress hanya bekerja pada lapisan HTTP(S) dimana ia melakukan pemetaan ke Service berdasarkan path di URL. Namun, ada kalanya layanan non-HTTP juga perlu dipublikasikan. Layanan TCP/UDP non-HTTP tidak mengenal konsep URL yang hanya ada di aplikasi web. Lalu, bagaimana bila ingin melakukan hal yang sama seperti di Ingress tetapi pemetaan dilakukan berdasarkan nomor port?

Untuk menunjukkan permasalahan ini secara jelas, saya akan membuat dua StatefulSet baru yang menerima koneksi dari port TCP yang berbeda. Pada percobaan sederhana ini, saya akan menggunakan nc -lp untuk mewakili sebuah layanan yang menerima koneksi di port tertentu. Sebagai contoh, ini adalah definisi untuk layanan-xyz-port-10000:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: layanan-xyz-port-10000
spec:
  serviceName: layanan-xyz-port-10000
  replicas: 1
  selector:
    matchLabels:
      app: layanan-xyz
      jenis: port-10000
  template:
    metadata:
      labels:
        app: layanan-xyz
        jenis: port-10000
    spec:
      containers:
        - name: alpine
          image: alpine
          ports:
            - containerPort: 10000
          command: ["/bin/sh"]
          args: ["-c", "while true; do echo 'respon dari port 10000' | nc -lp 10000; done"]

Dan ini adalah definisi untuk layanan-xyz-port-20000:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: layanan-xyz-port-20000
spec:
  serviceName: layanan-xyz
  replicas: 3
  selector:
    matchLabels:
      app: layanan-xyz
      jenis: port-20000
  template:
    metadata:
      labels:
        app: layanan-xyz
        jenis: port-20000
    spec:
      containers:
        - name: alpine
          image: alpine
          ports:
            - containerPort: 20000
          command: [ "/bin/sh" ]
          args: [ "-c", "while true; do echo 'respon dari port 20000' | nc -lp 20000; done" ]

Salah satu pendekatan naif untuk mempublikasikan kedua StatefulSet di atas adalah dengan menggunakan selector app=layanan-xyz karena kedua StatefulSet di atas sama-sama memiliki label app=layanan-xyz. Sebagai contoh, saya akan mencoba membuat sebuah Service seperti berikut ini:

apiVersion: v1
kind: Service
metadata:
  name: layanan-xyz
  labels:
    app: layanan-xyz
spec:
  selector:
    app: layanan-xyz
  type: LoadBalancer
  ports:
    - port: 10000
      targetPort: 10000
      name: port-10000
    - port: 20000
      targetPort: 20000
      name: port-20000

Walaupun secara sintaks, tidak ada yang salah pada manifest di atas, terdapat sebuah kesalahan logika yang mungkin saja bisa terlewatkan. Service di atas akan menerima masukan pada port 10000 dan port 20000 lalu melewatkannya ke seluruh Pod yang memenuhi kriteria app=layanan-xyz (gabungan antara layanan-xyz-port-10000 dan layanan-xyz-port-20000) secara acak. Ini berarti ada kemungkinan request untuk port 10000 dilewatkan ke layanan-xyz-port-20000 dan juga sebaliknya. Karena layanan-xyz-port-20000 tidak menerima koneksi di port 10000, tentu saja ini akan menimbulkan pesan kesalahan “Connection refused”. Oleh sebab itu, bila saya mencoba melakukan koneksi berulang kali ke port 10000, akan ada kemungkinan saya menerima pesan kesalahan seperti berikut ini:

$ nc -v <ip_load_balancer> 10000

nc: connect to <ip_load_balancer> port 10000 (tcp) failed: Connection refused

Tergantung pada keberuntungan apakah koneksi akan diteruskan ke Pod yang benar, beberapa penggunaka akan mendapatkan respon sukses dan pengguna lainnya akan mendapatkan pesan kesalahan. Tentu saja saya tidak ingin aplikasi yang eksekusinya bergantung pada keberuntungan! Dengan demikian, Service di atas tidak dapat dipakai untuk melewatkan layanan melalui port berbeda dengan IP yang sama!

Salah satu solusi untuk permasalahan ini adalah dengan menggunakan Ingress Controller yang memiliki kapabilitas untuk meneruskan layanan TCP/UDP. Sebagai latihan, saya akan menggunakan ingress-nginx yang sudah memiliki kemampuan serupa. Karena definisi Ingress bawaan Kubernetes hanya mendukung sintaks pembagian berdasarkan path, sebagai gantinya, ingress-nginx menggunakan ConfigMap dengan sintaks-nya tersendiri yang hanya dimengerti oleh Ingress Controller tersebut.

Fitur penerusan TCP/UDP di ingress-nginx tidak diaktifkan secara default! Oleh sebab itu, saya perlu mengaktifkannya terlebih dahulu sebelum melakukan instalasi ingress-nginx. Sebagai contoh, saya melakukan perubahan pada file https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.3.1/deploy/static/provider/cloud/deploy.yaml dengan menambahkan baris --tcp-services-configmap di args untuk Deployment dengan nama ingress-nginx-controller:

#... <tidak disertakan> ...
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
    app.kubernetes.io/version: 1.3.1
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  # ... <tidak disertakan> ...
    spec:
      containers:
      - args:
        - /nginx-ingress-controller
        - --publish-service=$(POD_NAMESPACE)/ingress-nginx-controller
        - --election-id=ingress-controller-leader
        - --controller-class=k8s.io/ingress-nginx
        - --ingress-class=nginx
        - --configmap=$(POD_NAMESPACE)/ingress-nginx-controller
        - --validating-webhook=:8443
        - --validating-webhook-certificate=/usr/local/certificates/cert
        - --validating-webhook-key=/usr/local/certificates/key
        - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
  # ... <tidak disertakan> ...

Selain itu, saya juga perlu mempublikasikan port yang dibutuhkan dengan menambahkannya di bagian Service ingress-nginx-controller:

#... <tidak disertakan> ...
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
    app.kubernetes.io/version: 1.3.1
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  # ... <tidak disertakan> ...  
  ports:
    # ... <tidak disertakan> ...  
  - name: port-10000
    port: 10000
    targetPort: 10000
    protocol: TCP
  - name: port-20000
    port: 20000
    targetPort: 20000
    protocol: TCP
  # ... <tidak disertakan> ...

Sekarang, saya siap melakukan instalasi dengan memberikan perintah kubectl apply -f deploy.yaml dimana deploy.yaml mewakili file manifest yang telah saya modifikasi di atas.

Berikutnya, saya perlu membuat Service lokal yang mewakili masing-masing layanan yang hendak diakses melalui ingress-nginx.
Sebagai contoh, saya membuat Service dengan isi seperti berikut ini untuk menerima pesan di port 10000:

apiVersion: v1
kind: Service
metadata:
  name: layanan-xyz-port-10000
spec:
  type: ClusterIP
  selector:
      app: layanan-xyz
      jenis: port-10000
  ports:
    - protocol: TCP
      port: 10000
      targetPort: 10000

Saya juga melakukan hal serupa untuk membuat Service yang menerima pesan di port 20000:

apiVersion: v1
kind: Service
metadata:
  name: layanan-xyz-port-20000
spec:
  type: ClusterIP
  selector:
      app: layanan-xyz
      jenis: port-20000
  ports:
    - protocol: TCP
      port: 20000
      targetPort: 20000

Sebagai langkah terakhir, saya perlu membuat ConfigMap dengan nama tcp-services yang mewakili pemetaan dari port TCP ke Service yang bersangkutan dengan isi seperti berikut ini:

apiVersion: v1
kind: ConfigMap
metadata:
  name: tcp-services
  namespace: ingress-nginx
data:
  10000: "default/layanan-xyz-port-10000:10000"
  20000: "default/layanan-xyz-port-20000:20000"

Pada konfigurasi di atas, default/layanan-xyz-port-10000 dan default/layanan-xyz-port-20000 adalah nama Service yang saya buat pada langkah sebelumnya. Keduanya berada di namespace default bawaan (ini adalah namespace yang dipakai bila tidak ditentukan).

Sekarang, bila saya mengakses port 10000 dari IP milik ingress-nginx-controller, saya akan selalu mendapatkan respon dari Service yang benar tanpa pesan kesalahan “Connection refused” lagi:

$ nc <ip_ingress_controller> 10000

respon dari port 10000

Begitu juga bila saya mengakses port 20000 dari IP tersebut, saya tidak akan menemukan pesan kesalahan lagi:

$ nc <ip_ingress_controller> 20000

respon dari port 20000