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

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

![fig1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46847/35ba0a62-4b2b-9850-75e3-6e9f510c865b.png)

[Service | Kubernetes](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer)

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

![fig2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46847/995188dc-d9e5-c94d-d50d-65d05e639aff.png)

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

![fig3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46847/7c438873-0ae2-c6a6-ec55-3c29a37ca693.png)

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

![fig4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46847/5ecf0ebd-8d98-09aa-1372-514bf97dd177.png)

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

[NGINX Docs | TCP and UDP Load Balancing](https://docs.nginx.com/nginx/admin-guide/load-balancer/tcp-udp-load-balancer/)

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

![fig5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46847/421617ac-8975-0042-7878-bc56178f890c.png)

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

[Exposing TCP and UDP services - NGINX Ingress Controller](https://kubernetes.github.io/ingress-nginx/user-guide/exposing-tcp-udp-services/)

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

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

```yml
---
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 に記載する

```yml
---
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を無視して独自にキャッシュを持つため、下記のように短めに設定しなおします。

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

参考: [nginx の名前解決について - Qiita](https://qiita.com/toshihirock/items/1c711a7e9f054605323f)

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

```yml
---
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などは省略）

```yml
---
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を使うこともできます。

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

[Network Load Balancer Support in Kubernetes 1.9 | AWS Open Source Blog](https://aws.amazon.com/jp/blogs/opensource/network-load-balancer-support-in-kubernetes-1-9/)

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

[特徴 - Elastic Load Balancing | AWS](https://aws.amazon.com/jp/elasticloadbalancing/features/)

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

[Network Load Balancer のクォータ - Elastic Load Balancing](https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/network/load-balancer-limits.html)
[Classic Load Balancer のクォータ - Elastic Load Balancing](https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/classic/elb-limits.html)

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