NGINX Ingress Controller を利用して EKS 上のアプリケーションを HTTPS で公開してみた (ACM を利用して NLB で SSL 終端するパターン )

EKS 上のアプリケーションを外部公開する際に使う Ingress コントローラとして ALB Load Balancer Controller や NGINX Ingress Controller が挙げられます。
主な違いはリバースプロキシとしてカスタマイズ性の高い nginx を利用するか AWS マネージドのサービスである Elastic Load Balancing を利用するかになります。
今回はこれらのコントローラの違いについて詳しく触れませんが、詳細は下記 Amazon Web Services ブログが参考になります。

NGINX Ingress Controller を採用するとなった場合、 NLB で SSL 終端して ACM を利用するパターンと AWS 外で取得した証明書を Secret として登録して利用するパターンがあります。
ACM を利用することで、nginx を利用しつつも証明書の管理や更新を AWS に任せることができます。
今回は NGINX Ingress Controller を利用しつつ、NLB で SSL 終端するパターンを試してみました。

EKS クラスター作成

eksctl でクラスターを作成します。

eksctl create cluster --name test-cluster --region ap-northeast-1 --version 1.24 --vpc-cidr 10.0.0.0/16 --without-nodegroup --with-oidc --zones ap-northeast-1a,ap-northeast-1c

続いてノードグループも作成します。

eksctl create nodegroup --cluster test-cluster --region ap-northeast-1 --name test-cluster-ng --node-ami-family AmazonLinux2 --node-type t3.medium --nodes 2 --nodes-min 1 --nodes-max 2 --node-private-networking

証明書の作成

次に利用する証明書を ACM で発行します。
今回はホストベースルーティングを利用したかったためワイルドカード証明書を作成しました。

NGINX Ingress Controller のインストール

Installation Guide に沿って、 NGINX Ingress Controller をインストールします。

wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/aws/deploy.yaml

ダウンロードしたファイルの一部を書き換えます。
ConfigMap(ingress-nginx-controller) の proxy-real-ip-cidr を EKS クラスターがデプロイされている VPC の CIDR に変更します。
Service(ingress-nginx-controller) の service.beta.kubernetes.io/aws-load-balancer-ssl-cert を ACM で発行した証明書の ARN に変更します。

※ ここで Service(ingress-nginx-controller) の externalTrafficPolicyCluster に設定しておかないと、 Ingress Controller が存在する Node にある Pod にしかトラフィックを流さなくなります。必ずしも変更する必要はありませんが、気になった場合は変更して下さい。

修正が終わったら、 NGINX Ingress Controller をデプロイします。

$ kubectl apply -f deploy.yaml
namespace/ingress-nginx created
serviceaccount/ingress-nginx created
serviceaccount/ingress-nginx-admission created
role.rbac.authorization.k8s.io/ingress-nginx created
role.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrole.rbac.authorization.k8s.io/ingress-nginx created
clusterrole.rbac.authorization.k8s.io/ingress-nginx-admission created
rolebinding.rbac.authorization.k8s.io/ingress-nginx created
rolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
configmap/ingress-nginx-controller created
service/ingress-nginx-controller created
service/ingress-nginx-controller-admission created
deployment.apps/ingress-nginx-controller created
job.batch/ingress-nginx-admission-create created
job.batch/ingress-nginx-admission-patch created
ingressclass.networking.k8s.io/nginx created
validatingwebhookconfiguration.admissionregistration.k8s.io/ingress-nginx-admission created

アプリケーションのデプロイ

まず、アプリケーション用の名前空間を作成します。

$ kubectl create namespace apps
namespace/apps created

次に Service リソースを作成します。
今回は Google が公開している hello と表示するだけのコンテナイメージを利用します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: first
  namespace: apps
  labels:
    app.kubernetes.io/name: first
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: first
  replicas: 2
  template:
    metadata:
      labels:
        app.kubernetes.io/name: first
    spec:
      terminationGracePeriodSeconds: 0
      containers:
        - name: first
          image: gcr.io/google-samples/hello-app:1.0
          ports:
            - name: app-port
              containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: first
  namespace: apps
  labels:
    app.kubernetes.io/name: first
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: first
  ports:
    - name: svc-port
      port: 3000
      targetPort: app-port
      protocol: TCP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: second
  namespace: apps
  labels:
    app.kubernetes.io/name: second
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: second
  replicas: 2
  template:
    metadata:
      labels:
        app.kubernetes.io/name: second
    spec:
      terminationGracePeriodSeconds: 0
      containers:
        - name: second
          image: gcr.io/google-samples/hello-app:1.0
          ports:
            - name: app-port
              containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: second
  namespace: apps
  labels:
    app.kubernetes.io/name: second
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: second
  ports:
    - name: svc-port
      port: 3001
      targetPort: app-port
      protocol: TCP
$ kubectl apply -f service.yaml
deployment.apps/first created
service/first created
deployment.apps/second created
service/second created

Ingress リソースをデプロイします。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: apps-ingress
  namespace: apps
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/use-regex: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: 100m
spec:
  rules:
    - host: first.masutaro99.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: first
                port:
                  number: 3000
    - host: second.masutaro99.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: second
                port:
                  number: 3001
$ kubectl apply -f ingress.yaml
ingress.networking.k8s.io/apps-ingress created

レコード作成

Route53 で設定したホスト名 (first.masutaro99.com, second.masutaro99.com) からNLB へのエイリアスレコードを作成します。

動作確認

それぞれのホストに対してリクエストを送り、それぞれ HTTPS で接続できていることを確認できました。

$ curl -v https://first.masutaro99.com
*   Trying 57.180.91.200:443...
* Connected to first.masutaro99.com (57.180.91.200) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (OUT), TLS handshake, Client hello (1):
* (304) (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject: CN=*.masutaro99.com
*  start date: Oct 22 00:00:00 2023 GMT
*  expire date: Nov 19 23:59:59 2024 GMT
*  subjectAltName: host "first.masutaro99.com" matched cert's "*.masutaro99.com"
*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M02
*  SSL certificate verify ok.
> GET / HTTP/1.1
> Host: first.masutaro99.com
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Sun, 22 Oct 2023 12:38:35 GMT
< Content-Type: text/plain; charset=utf-8
< Content-Length: 62
< Connection: keep-alive
<
Hello, world!
Version: 1.0.0
Hostname: first-7c5db465f5-jsmdb
* Connection #0 to host first.masutaro99.com left intact
$ curl -v https://second.masutaro99.com
*   Trying 57.180.91.200:443...
* Connected to second.masutaro99.com (57.180.91.200) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (OUT), TLS handshake, Client hello (1):
* (304) (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject: CN=*.masutaro99.com
*  start date: Oct 22 00:00:00 2023 GMT
*  expire date: Nov 19 23:59:59 2024 GMT
*  subjectAltName: host "second.masutaro99.com" matched cert's "*.masutaro99.com"
*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M02
*  SSL certificate verify ok.
> GET / HTTP/1.1
> Host: second.masutaro99.com
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Sun, 22 Oct 2023 12:39:08 GMT
< Content-Type: text/plain; charset=utf-8
< Content-Length: 63
< Connection: keep-alive
<
Hello, world!
Version: 1.0.0
Hostname: second-64966675f9-dtr4g
* Connection #0 to host second.masutaro99.com left intact