ALB IngressとKong Ingress Controller(KIC)を繋げてALB経由でKICを利用する

本記事は「Kong Advent Calendar 2024」の8日目のエントリとして、ALB IngressとKong Ingress Controllerを組み合わせて利用する方法について解説する。

EKSを使っている多くの人はALB Ingress Controllerを使っているのではないかと思う。
これを使うとIngress作成時にALBが自動で作成され、IP制限やACMとの証明書連携などが簡単に出来る優れモノである。
一方で、Kong Gatewayを使っている場合、Kong Ingress Controller（KIC）も非常に便利である。
KICの場合はIngressを作成するとKong Gateway側にService/Routeを自動作成してくれるので、Ingressで公開するサービスのレート制限などが簡単に行えるようになる。

比較するとおおよそ以下のような感じになると思う。

||ALB Ingress|KIC|
|---|---|---|
|主目的|Ingress公開時にALBのプロビジョニングも行う|KubernetesリソースとしてKongのエンティティを管理|
|サービスの公開方法|個々のALBのエンドポイント|DPのエンドポイント(type:LoadBalancer)|
|設定の反映先|ALB|Kong Gateway|
|設定方法|Annotation|Annotation, CRD(KongIngress)|
|設定出来ること|IP制限、ALBの向き先、etc|Plugin、strip-path、プロトコル、etc|
|証明書利用の推奨方法|ACM|cert-manager|
|利用シーン|クラスタのアクセスにALBが必要な場合|Kong Gatewayの設定をKubernetesネイティブに行いたい場合|

ALB使いたいしKong Gatewayも使いたい、ってなった場合にネックになるのが、Ingressそのものは多段接続出来ない点である。
![20241114101431.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/47e62daa-97fb-c0ce-485e-ee7ed4d6caa4.png)

ただ、Kong GatewayのProxyはIngressではなくServiceで提供されることを考えると、上手くやれば以下のような構成が取れるのではないかと考えた。

![20241120152135.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/7ab91e63-0728-e9c1-25d7-ccac25d7fb40.png)

実際に検証してみる。

## 前提
検証の前提として以下で実施する。
- EKSを利用
- ALB Ingress Controllerは導入済み
- Proxyに使うIngressはLet's Encryptの証明書を利用する 
- ALB IngressはAWS Certificate Manager（ACM)から証明書を取得する
- Proxyが使うドメインとKICで提供するサービスのドメインは同一のドメインとする（サブドメインは異なる）

ProxyのドメインとKICで提供するサービスのドメインを共通化しているのは、証明書やIngressの用意が大変になるからである。
ドメインを分けたい場合はここでの作成手順で作った後、Ingressをコピーして別ドメイン用にカスタマイズして提供すれば上手くいくんじゃないかと思う。

## 検証
検証するにあたり、環境によって変更するような値を環境変数で定義しておく。
```sh
# ControlPlaneのHelmのリリース名
CP_RELEASE_NAME=kong-aws-cp
# DataPlaneのHelmのリリース名
DP_RELEASE_NAME=kong-aws-dp
# Ingressでアクセスする際のドメイン
DOMAIN=eks.hogehoge.info
```
### 証明書の払い出しとACMへの登録
ここでの証明書の払い出しはあくまでも一例であり、これでなくてはならない、というものでもないので参考程度に見て欲しい。
以下、`certbot`コマンドを使ってワイルドカード証明書として証明書を作成する。
```sh
EMAIL=mymail@hogehoge.info
sudo certbot certonly --manual \
     --preferred-challenges dns-01 \
     --server https://acme-v02.api.letsencrypt.org/directory \
     -m $EMAIL \
     -d "*.$DOMAIN"
```
作成し終わったら、ACMに証明書をインポートする。
![20241114104745.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/b3ad4580-8c31-a0e9-1179-ce7648db5722.png)

インポート後、証明書のARNを後で使うので、環境変数に設定する。

```sh
CERT_ARN="arn:aws:acm:us-east-1:xxxx:certificate/9d8bbc9b-142c-4329-96d0-xxxx"
```

### ControlPlaneのデプロイ
次にControlPlaneをデプロイする。
ControlPlaneのデプロイは以前紹介した「[Kong GatewayをKubernetes上でHybridモードで構築する（cert-manager使用）](https://qiita.com/ipppppei/items/a70f3a859af509ec8bef#controlplane%E3%81%AE%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4)」の手順でデプロイする。
全く同じ手順なので`values.yaml`やその説明については割愛する。
ControlPlane構築後、Ingress Controllerが展開されるがDataPlaneはまだ展開されていないのでProxyが利用できないところも同じとなる。

### DataPlaneのデプロイ
DataPlaneをデプロイする。
今回デプロイに使ったvalues.yamlは以下となる。

<details><summary>values.yaml（クリックして展開）：</summary><div>

```yaml:dp-alb.yaml
cat <<EOF > ./dp-alb.yaml
image:
  repository: kong/kong-gateway
env:
  role: data_plane
  database: "off"
  cluster_control_plane: ${CP_RELEASE_NAME}-kong-cluster.kong.svc:8005
  cluster_telemetry_endpoint: ${CP_RELEASE_NAME}-kong-clustertelemetry.kong.svc.cluster.local:8006
cluster:
  enabled: true
  tls:
    enabled: true

certificates:
  enabled: true
  issuer: "my-ca-issuer"
  cluster:
    enabled: true
    commonName: "$DOMAIN"
    dnsNames:
    - "*.$DOMAIN"
proxy:
  enabled: true
  type: ClusterIP
  http:
    enabled: true
  tls:
    enabled: true
  ingress:
    enabled: true
    ingressClassName: alb
    hostname: "*.$DOMAIN"
    path: /*
    pathType: ImplementationSpecific
    tls:
    - hosts:
      - proxy.$DOMAIN
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/backend-protocol: HTTPS
      alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS": 443},{"HTTP": 80}]'
ingressController:
  enabled: false
postgresql:
  enabled: false
migrations:
  enabled: false
manager:
  enabled: false
admin:
  enabled: false
EOF
```
</div></details>

基本的な解説は[以前の記事](https://qiita.com/ipppppei/items/a70f3a859af509ec8bef#dataplane%E3%81%AE%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4)で紹介済みなので、ここではProxyのブロックだけ解説する。
```yaml
proxy:
  enabled: true
  type: ClusterIP
```
ここは従来だと`type: LoadBalancer`だがここでは`type: ClusterIP`を指定している。
Proxyとしての公開先はALB Ingressのみであり、クラスタ内通信で十分なので`ClusterIP`とした。
なお、当初はALB Ingressの使用で最初のアーキテクチャ図に記載したように`NodePort`で検討していたが、`alb.ingress.kubernetes.io/target-type: ip`を指定すれば`ClusterIP`で問題ないので`NodePort`は使用しなかった。
（多分`NodePort`でも利用できるが、`NodePort`の仕組み上性能面では`ClusterIP`に劣る上、セキュリティ観点やポート消費の観点から`ClusterIP`の方が望ましい）

```yaml
  ingress:
    enabled: true
    ingressClassName: alb
    hostname: "*.$DOMAIN"
```
ここが一番の肝となる。
Ingressを有効化し、Ingress Controllerに`alb`を指定してALB Ingressを利用する。
また`hostname`がワイルドカードになっているのも重要で、**KICで公開するホスト名がこのhostnameと一致する必要がある。**
これが一致しないとIngressがそもそもProxyに転送してくれず、以下のようなエラーを見続けることになる（検証時にハマった）。
```sh
HTTP/1.1 404 Not Found
Server: awselb/2.0
```

次にTLS箇所を見る
```yaml
    tls:
    - hosts:
      - proxy.$DOMAIN
```
これを指定しないと443で待ってくれないので指定する。
ワイルドカード証明書を使うようにしていれば、`hosts`に書くホスト名は1つあれば十分だが、ワイルドカード証明書を使わない場合はKICで使うホスト名を列挙する感じになると思う。

```yaml
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/backend-protocol: HTTPS
      alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS": 443},{"HTTP": 80}]'
```
Annotationについては以下の目的で設定している。
- `alb.ingress.kubernetes.io/scheme: internet-facing`：インターネット経由で利用したいので`internet-facing`を指定
- `alb.ingress.kubernetes.io/backend-protocol: HTTPS`：ProxyにHTTPSで転送するので`HTTPS`を指定
- `alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN`：ACMのARNを指定し、ACM上の証明書を利用
- `alb.ingress.kubernetes.io/target-type: ip`：`type: ClusterIP`を利用するために指定
- `alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS": 443},{"HTTP": 80}]'`：ALB側（≠Ingress側）で待ち受けるポートの指定。HTTPでの待ち受けも許可するために`HTTP:80`を指定

ALB側でIP制限をしたい場合などはここに設定を追加する。
`values.yaml`による設定が終わったら、DataPlaneをデプロイする。
```sh
helm upgrade -i kong-aws-dp kong/kong -f ./dp-alb.yaml -n kong --wait
```
デプロイ後、Ingressは以下のような感じになる。
```sh
$ kubectl get ing -n
NAME                       CLASS   HOSTS                    ADDRESS                                                               PORTS     AGE
kong-aws-cp-kong-admin     kong    kong.eks.hogehoge.info   10.100.73.101                                                         80, 443   6d
kong-aws-cp-kong-manager   kong    kong.eks.hogehoge.info   10.100.73.101                                                         80, 443   6d
kong-aws-dp-kong-proxy     alb     *.eks.hogehoge.info      k8s-kong-kongawsd-c997c9b60c-xxxx.us-east-1.elb.amazonaws.com   80, 443   37m
```
KICを使っているKong ManagerとAdminAPIの`HOSTS`はDPのProxyのServiceのIPを指している。
これは先程`values.yaml`で`proxy.service`に`ClusterIP`を指定したからである。
実際はこのアドレスは使わず、DNS側で宛先を指定する。
ということで、DataPlaneのデプロイが終わったらDNSで`*.eks.hogehoge.info`参照時はALBのホストとなる`k8s-kong-kongawsd-c997c9b...`を使うよう設定する。

### 動作確認
他にKICを使っているものがなければ、デプロイ後は以下のように動作するはずだ。
- `https://kong.eks.hogehoge.info`：Kong Managerへのアクセス
- `https://kong.eks.hogehoge.info/api`：Admin APIへのアクセス
- `https://<kong以外>.eks.hogehoge.info`：Proxyへのアクセス

KICの仕組みを知っていないとピンと来ないかも知れないので、補足として以下の絵を足しておく。
<img alt="picture 6" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/9510d40a-2ad5-6823-f4b1-49c0a628a417.png" /> 

上記の認識通り動くか確認する。
まずブラウザで`https://kong.eks.hogehoge.info`にアクセスするとKong Managerが表示された。
![20241120164846.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/ff694e57-3e93-8661-7675-54f0d6473e4a.png)

次にcurlで`https://kong.eks.hogehoge.info/api`にアクセスすると、Kong Gatewayの設定値が表示された。
```json
$ curl https://kong.eks.hogehoge.info/api
{"version":"3.7.1.2","plugins":{"disabled_on_server":{},"enabled_in_cluster":["prometheus"],"available_on_server":{"statsd":{"version":"3.7.1","priority":11},"bot-detection":{"version":"3.7.1","priority":2500},"aws-lambda":{"version":"3.7.1","priority":750},"request-termination":
：（省略）
```
Admin APIは`/`にアクセスすると設定値を表示するのでこちらも問題なさそうだ。
最後にProxyも確認する。
```sh
$ curl https://proxy.eks.hogehoge.info/
{
  "message":"no Route matched with those values",
  "request_id":"5a60e547d6d64e34fc22783ab4b1d210"
}
```
こちらも問題なさそうだ。

ここでKIC経由でKong Gatewayを正常に操作出来るか確認するために、ServiceやRouteを足してみる。ここではKongがホスティングしている[httpbin](https://httpbin.konghq.com)を使って確認する。
```sh
curl -s -X POST https://kong.eks.hogehoge.info/api/services \
  -H "Content-Type: application/json" \
  -d '{
    "name":"httpbin-svc",
    "url":"https://httpbin.konghq.com"
    }'
```
```sh
curl -s -X POST https://kong.eks.hogehoge.info/api/services/httpbin-svc/routes \
  -H "Content-Type: application/json" \
  -d '{
    "name":"httpbin-rt",
    "paths":["/httpbin"]
    }'
```
設定後、アクセスすると正常に使えることが分かる。
```sh
$ curl https://proxy.eks.hogehoge.info/test/user-agent
{
  "user-agent": "curl/8.7.1"
}
```

KICも使ってみる。Ingressの確認では定番の[kuard](https://github.com/kubernetes-up-and-running/kuard)を利用する。
以下でNamespace、Service、Deploymentを作成する。
```yaml
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Namespace
metadata:
  name: demo
---
apiVersion: v1
kind: Service
metadata:
  name: kuard
  namespace: demo
spec:
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
  selector:
    app: kuard
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: kuard
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kuard
  template:
    metadata:
      labels:
        app: kuard
    spec:
      containers:
        - name: kuard
          image: gcr.io/kuar-demo/kuard-amd64:1
          ports:
            - containerPort: 8080
EOF
```
kuardにアクセスするためのIngressを作成する。
```yaml
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kuard
  namespace: demo
spec:
  ingressClassName: kong
  rules:
    - host: kuard.$DOMAIN
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: kuard
                port:
                  number: 80
EOF
```
`ingressClassName`で`kong`(KIC)を指定し、`host`で`kuard.eks.hogehoge.info`にアクセスすればkuardのServiceに届くよう指定した。
Ingress作成後、`http://kuard.eks.hogehoge.info`にアクセスする。
![20241120172644.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/860c448d-16bc-232f-a945-7530f3ea3069.png)

無事にアクセスできた。
Kong ManagerからRouteを確認すると、kuard向けのRouteが適切に作成されていることも確認できる。
![20241120172803.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/eef4f508-36bc-ffd8-07e7-2c74170926c8.png)

ということで、ALBとKICの組み合わせは上手く動いた。

## おわりに
設定はかなり複雑なところがあるが、設定できてしまえばALBを使いつつKICが使えるという美味しいどこどりが出来るので、EKSでKICを使う場合はこの構成も検討してみてもいいと思う。