Kubernetesで複数のTCP接続をNGINXを使って1つのLBにまとめる


Kubernetes の Ingress は、外部のトラフィックを受けるのはHTTPのみでTCPやUDPにはまだ対応していません。
現状では、それらのトラフィックを受けるには Service の type を loadBalancer に設定することでTCPなどの接続を受けることができるようになります。

Service | Kubernetes

1 Service = 1 LB

ただ、Service の場合、Ingress のように条件に応じて振り分けるようなことができないため、1つの Service で1つのPodに割り振るような設定しかできません。

EKS (Amazon Elastic Kubernetes Service) の場合、Service に LoadBalancer を設定すると CLB (Classic Load Balancer) か NLB (Network Load Balancer) が1つ割り当てられます。

CLBやNLBは稼働させるだけなら1台あたり2,000円/月くらいなのですが、開発環境などで複数環境が必要になってくると、それなりの数のServiceを稼働させることになるので、お値段も気にしなるところです。

複数のServiceを1つのLBにまとめたい

NGINX では http 以外にも stream{} ブロックを記載することで、TCP や UDP もロードバランシングすることができます。今回の要件では、この機能を使って実現したいと思います。

NGINX Docs | TCP and UDP Load Balancing

今回は下記の図のように開発環境でTCP接続が必要なアプリが複数ある場合に、1つのLBで複数のポートを開けて、NGINXで複数のServiceに振り分ける方法でやっていきます。

NGINX Ingress Controller でも同様の機能があるのですが、Ingressの機能などは不要なのと、特に設定も難しくないため1からNGINXを設定して対応します。

Exposing TCP and UDP services - NGINX Ingress Controller

設定してみる

まずは外部のトラフィックを受ける Service です。受信するポートは 40004001 に設定。
external-dns も動いているので、 annotations でドメインの設定も行行います。

GitHub - kubernetes-sigs/external-dns: Configure external DNS servers (AWS Route53, Google CloudDNS and others) for Kubernetes Ingresses and Services

---
apiVersion: v1
kind: Service
metadata:
  name: nginx-sv
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "tcp"
    service.beta.kubernetes.io/aws-load-balancer-extra-security-groups: "sg-xxxxxx"
    service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: "3600"
    external-dns.alpha.kubernetes.io/hostname: "proxy.example.com"
spec:
  type: LoadBalancer
  selector:
    app: nginx
  ports:
  - port: 4000
    targetPort: 4000
    name: app0
    protocol: TCP    
  - port: 4001
    targetPort: 4001
    name: app1
    protocol: TCP

NGINX の設定ファイルを ConfigMap に記載する

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf
data:
  nginx-stream.conf: |
    user  nginx;
    worker_processes  1;

    error_log  /var/log/nginx/error.log warn;
    pid        /var/run/nginx.pid;

    events {
      worker_connections  1024;
    }

    stream {
      resolver kube-dns.kube-system.svc.cluster.local valid=5s;

      proxy_timeout 60m;
      proxy_socket_keepalive on;

      server {
        listen 4000;
        proxy_pass app0.default.svc.cluster.local:6000;
      }

      server {
        listen 4001;
        proxy_pass app1.default.svc.cluster.local:6000;
      }
    }

NGINXではDNSのTTLを無視して独自にキャッシュを持つため、下記のように短めに設定しなおします。

resolver kube-dns.kube-system.svc.cluster.local valid=5s;

参考: nginx の名前解決について - Qiita

NGINX の Deployment では上記のConfigMapをVolumeMountして読み込ませる

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-dp
spec:
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx:1.17
        name: nginx
        resources:
          limits:
            cpu: 500m
            memory: 250Mi
        ports:
        - containerPort: 4000
          name: app0
        - containerPort: 4001
          name: app1
        volumeMounts:
        - mountPath: /etc/nginx/nginx.conf
          subPath: nginx.conf
          name: nginx-conf
      volumes:
      - configMap:
          name: nginx-conf
          items:
          - key: nginx-stream.conf
            path: nginx.conf
        name: nginx-conf

アプリ側のService(Deploymentなどは省略)

---
apiVersion: v1
kind: Service
metadata:
  name: app0
spec:
  ports:
  - port: 4000
---
apiVersion: v1
kind: Service
metadata:
  name: app1
spec:
  ports:
  - port: 4001

これで proxy.example.com の 4000 ポートにアクセスすると app0 に、4001 ポートにアクセスすると app1 に到達するようになりました。

まとめ

NGINXを使うことで、ロードバランサーを集約することができました。
EKS の場合、Service の type が LoadBalancer の場合、デフォルトでは CLB になりますが、annotations に以下のように設定することで NLBを使うこともできます。

annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "nlb"

Network Load Balancer Support in Kubernetes 1.9 | AWS Open Source Blog

CLB と NLB では AWS のドキュメントにあるように機能差があります。パフォーマンスについても NLB の方が優れているようです。

特徴 - Elastic Load Balancing | AWS

また、上記には記載がありませんが、設定できるリスナーの上限は NLB は 50 で CLB は 100 になります。

Network Load Balancer のクォータ - Elastic Load Balancing
Classic Load Balancer のクォータ - Elastic Load Balancing

今回は開発環境ということでパフォーマンスはそれほど重視していないのと、アクセス制限を Security Group で行いたかったので CLB を使うことにしました。
現状では NLB は Security Group に対応していませんが、最近、TLS ターミネーションに対応したので、近い将来 Serucity Group にも対応してくれるかもしれません。