cert-managerでTLS化したKong Gatewayを構築する

[Kong Gateway](https://github.com/Kong/kong)をHelmで構築する際、デフォルトでKongは自己署名証明書を作成して起動する。
ただ、この証明書の管理を[cert-manager](https://cert-manager.io/)が生成する証明書に置き換えることも出来る。
そうすることで、証明書の管理がManifestベースで可能となり、cert-managerによる自動更新も働くため管理コストが抑えられる。
これの検証に手間取ったので、記録がてら手順を残す。

なお、検証を行うにあたり以下は用意済みのものとする。
- helmコマンド
- Kubernetesクラスタの構築
- `type: LoadBalancer`を使うためのLBの用意

ここではKubernetesはvSphere with Tanzu（Kubernetes：v1.25.7）を利用した。
また、以降でKICという言葉が出てくるが、これは[Kong Ingress Controller](https://github.com/Kong/kubernetes-ingress-controller)の略である。
## 事前準備：cert-managerのインストールとIssuerの準備
Helmでサクッとインストールする。
```sh
helm repo add jetstack https://charts.jetstack.io --force-update
helm repo update
helm install cert-manager jetstack/cert-manager --namespace cert-manager  --create-namespace --set installCRDs=true
```

証明書を発行するための`Issuer`を作成する。
今回は自己署名証明書で進める。
```sh
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned-issuer
spec:
  selfSigned: {}
EOF
```

## Kong Gatewayの構築
今回、ドメインを用意するのが面倒だったので、ワイルドカードDNSサービスの[nip.io](https://nip.io)を使う。
また、AdminAPIとKong Managerのホスト名は以下とする。
- kong-admin.\<KICのExternalIPをハイフンで繋いだもの\>.nip.io
- kong-manager.\<KICのExternalIPをハイフンで繋いだもの\>.nip.io

検証環境ではKICのExternalIPが事前に分からなかったため、以下の流れで構築する。
1. Helmの`values.yaml`を使ってKong GatewayとKICを立て、IPを取得
2. KICのIPを`values.yaml`に反映
3. 更新した`values.yaml`を使ってIngressを再デプロイ

最初にHelmの`values.yaml`を作成する。
作成にあたり、[公式のサンプル集](https://github.com/Kong/charts/tree/main/charts/kong/example-values)を参考にした。

```yaml
cat <<EOF > kong-values.yaml
env:
  database: "postgres"
admin:
  enabled: true
  annotations:
    konghq.com/protocol: "https"
  ingress:
    enabled: true
    ingressClassName: kong
    tls: kong-kong-admin-cert
    hostname: kong-admin.10-214-154-167.nip.io
postgresql:
  enabled: true
certificates:
  enabled: true
  clusterIssuer: "selfsigned-issuer"
  admin:
    commonName: "10-214-154-167.nip.io"
    dnsNames:
    - "*.10-214-154-167.nip.io"
manager:
  annotations:
    konghq.com/protocol: "https"
  ingress:
    enabled: true
    ingressClassName: kong
    tls: kong-kong-admin-cert
    hostname: kong-manager.10-214-154-167.nip.io
EOF
```
設定で試行錯誤したのでいくつか補足説明をする。
```yaml
  annotations:
    konghq.com/protocol: "https"
```    
上記のannotationは`Service`に設定され、これがないとKICがプロトコルをhttpとして扱ってしまい、`"400 The plain HTTP request was sent to HTTPS port`が表示されて上手くいかないので設定する必要がある。

```yaml
    tls: kong-kong-admin-cert
```
上記は`Ingress`をTLS化する際に証明書を渡すパラメータであり、cert-managerの`kind: Certificate`によって自動生成される`Secret`名を指定する。
これは命名ルールがドキュメント等で明文化されていない気がするが、Helm Chart内で`<Helmのリリース名>-<コンポーネント名>-cert`として[定義されている](https://github.com/Kong/charts/blob/744df5fab522268e4c743df377c892fce55815b6/charts/kong/templates/certificate.yaml#L50)。
Helmのリリース名を`kong`以外にする場合は変更する必要があるので注意。

```yaml
    hostname: kong-admin.10-214-154-167.nip.io
```
これは前述のように後から`kong-admin.<KICのExternalIPをハイフンで繋いだもの>.nip.io`に置き換えるため、一旦仮で入れている。置換し易い文字列なら何でもOK。

```yaml
  clusterIssuer: "selfsigned-issuer"
```
これは先ほど作成したcert-managerの`ClusterIssuer`を設定している。
ここで定義したものは他のコンポーネント全てに利用されるが、コンポーネント毎に設定することも可能。

```yaml
    commonName: "10-214-154-167.nip.io"
    dnsNames:
    - "*.10-214-154-167.nip.io"
```
証明書の内容を指定する。ここも後で置換する。
なお、KongのHelmの設定でデフォルトでKICもデプロイされるようになっているため、この`values.yaml`を適用するとKICもセットでデプロイされる。
（1Pod内にKong GatewayとKICそれぞれのイメージが含まれる）

作成した`values.yaml`を使ってKong GatewayとKICをデプロイする。
```sh
helm upgrade -i -f kong-values.yaml kong kong/kong -n kong --create-namespace
```
デプロイすると、以下のように`Ingress`が作成される。
```sh
$ kubectl get ing  -n kong
NAME                CLASS   HOSTS                                ADDRESS          PORTS     AGE
kong-kong-admin     kong    kong-admin.10-214-154-167.nip.io     10.214.154.169   80, 443   72s
kong-kong-manager   kong    kong-manager.10-214-154-167.nip.io   10.214.154.169   80, 443   72s
```
ワイルドカードDNSサービスを利用すると、`kong-admin.10-214-154-167.nip.io`にアクセスすると`10.214.154.167`にアクセスできる。
しかし、KICがProxyするアドレス(`10.214.154.169`)と異なるため、このホスト名ではアクセス出来ない。
そのため、`values.yaml`を書き換えて再デプロイする。
```sh
KIC_IP=$(kubectl get svc kong-kong-proxy -n kong -o jsonpath={.status.loadBalancer.ingress[0].ip} | sed "s/\./-/g")
sed -i "s/10-214-154-167/$KIC_IP/g" kong-values.yaml
helm upgrade -i -f kong-values.yaml kong kong/kong -n kong
```
問題なければ以下のように是正される。
```sh
$ kubectl get ing -n kong
NAME                CLASS   HOSTS                                ADDRESS          PORTS     AGE
kong-kong-admin     kong    kong-admin.10-214-154-169.nip.io     10.214.154.169   80, 443   8m42s
kong-kong-manager   kong    kong-manager.10-214-154-169.nip.io   10.214.154.169   80, 443   8m42s
```
これでKong ManagerとAdminAPIにアクセスできるようになる。
ただし、デプロイ直後はKong Managerにアクセスしても各種情報が適切に表示されず、Gateway Serviceなどにアクセスしても`Gateway Services could not be retrieved`といったエラーが表示される。
![1711096557803.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/0556f53f-7732-67e2-f197-7f9713cf75b9.png)

これの原因が分からず、デバッグレベルをあげてもよく分からなかったのだが、解決方法としてはAdminAPIにアクセスすると何故か治る。

※追記：
KongManagerからAdminAPIにアクセスしているが、CORSのせいでエラーになっている。
なので、一度AdminAPIにアクセスすると治る。
ドメインを同じにしてしまえば問題は起きないので、気になる人はとりあえずドメインを合わせるとよい。

以下のような感じでAdminAPI側にアクセスする。
![1711097965979.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/7b628eb2-0ed3-26ab-78f3-c1653c4626ee.png)
この後Kong Managerにアクセスすると適切に表示されるようになる。
![1711098032440.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/c892b62c-f5c3-3dc1-cfa8-48e45f358b75.png)
証明書もcert-managerにより発行されたものになっている。
![1711098073696.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/3fe570c8-dcb8-4ea8-52a7-0130b6a4b715.png)

## 動作確認
構築したものが正常に動作するか念の為確認する。
以前「[Kong AI Gatewayをdeck CLIで構築する](https://qiita.com/ipppppei/items/dc5de61a766830864d36)」というのを書いたが、これをこのKubernetes環境で動かしてみる。

最初に`env.sh`という環境変数を詰めたファイルを作成する。
[Cohere](https://cohere.com/)のAPIトークンに加え、`deck`の参照先Kong Gatewayを環境変数`DECK_KONG_ADDR`を使って設定する。
また、自己署名証明書なので`DECK_TLS_SKIP_VERIFY`で証明書のチェックをスキップする。
```sh
cat <<EOF > env.sh
export COHERE_API_KEY=qIzGBRKxxxxxxx
export DECK_KONG_ADDR=https://kong-admin.10-214-154-169.nip.io
export DECK_TLS_SKIP_VERIFY=true
EOF
. env.sh
```
次に上述の記事で実行した内容をスクリプト化して実行する。
なお、今回はせっかくなのでGateway ServiceをHTTPS化した。
```sh
cat <<'EoF' > ./run.sh
#!/bin/bash
set -x
. env.sh
deck gateway dump -o kong-base.yaml --yes
export SERVICE_NAME=my-service
export SERVICE_ADDR=${SERVICE_NAME}.10-214-154-169.nip.io
cat <<EOF > ./service.yaml
services:
- name: $SERVICE_NAME
  host: $SERVICE_ADDR
  port: 443
  protocol: https
EOF
deck gateway sync kong-base.yaml service.yaml
SERVICE_UUID=$(http ${DECK_KONG_ADDR}/services --verify no | jq '.data[] | select(.name == "'"$SERVICE_NAME"'")'.id)
echo $SERVICE_UUID
cat <<EOF > ./cohere-route.yaml
routes:
- name: cohere-api_cohere-chat
  service:
    id: $SERVICE_UUID
  paths:
  - "~/cohere-chat\$"
  methods:
  - POST
EOF
deck gateway sync kong-base.yaml service.yaml cohere-route.yaml
ROUTE_UUID=$(http ${DECK_KONG_ADDR}/routes --verify no | jq '.data[] | select(.name == "cohere-api_cohere-chat")'.id)
echo $ROUTE_UUID
cat <<EOF > ./cohere-ai-proxy.yaml
plugins:
- name: ai-proxy
  route: $ROUTE_UUID
  config:
    route_type: "llm/v1/chat"
    auth:
      header_name: "Authorization"
      header_value: "Bearer $COHERE_API_KEY"
    model:
      provider: "cohere"
      name: "command"
      options:
        max_tokens: 512
        temperature: 1.0
EOF
deck gateway sync kong-base.yaml service.yaml cohere-route.yaml cohere-ai-proxy.yaml
EoF
chmod +x ./run.sh
./run.sh
```
これでCohereへKong経由でAPIが投げられるようになっているので、試してみる。

```sh
curl -k -s --http1.1 -X POST https://my-service.10-214-154-169.nip.io/cohere-chat \
  -H "Content-Type: application/json" --data-raw '{
    "messages": [{
    "role": "user",
    "content": "Where is the capital of Japan?"
  }] }'
```

ちょっと内容が変な気もするが、それっぽい答えが返ってきた。
```json
{"id":"20ddf039-4f47-4f5e-a53f-b68f2488deaf","model":"command","choices":[{"message":{"content":"The capital of Japan is called Tokyo, it is the most populous metropolitan area in the world. \n\nTokyo is a very diverse city consisting of 23 city wards, multiple interwoven municipalities, and many neighborhoods. While blending traditional and modern cultures, it is known for its unique ambiance and ubiquitous skyscrapers, giving way to its popular name \"Tokyo Sky desper\" (Old Tokyotrapolis). \n\nTokyo will host the Summer Olympics of 2020, however, due to the COVID-19 pandemic, it will be postponed until summer of 2021. \n\nLet me know if you would like to know more about Tokyo or Japan!","role":"assistant"},"finish_reason":"stop","index":0}],"usage":{"completion_tokens":126,"prompt_tokens":69,"total_tokens":195},"object":"chat.completion"}
```

ちなみに`--http1.1`をつけない場合、`An unexpected error occurred`が返ってくる。
Proxyのログを見ると`buffered proxying cannot currently be enabled with http/2, please use http/1.x instead`と出ており、http/2だと上手く動かないようなので注意。
```sh
2024/03/22 09:16:29 [error] 1323#0: *3454 [kong] init.lua:405 [ai-proxy] /usr/local/share/lua/5.1/kong/plugins/ai-proxy/handler.lua:94: buffered proxying cannot currently be enabled with http/2, please use http/1.x instead, client: 192.168.2.1, server: kong, request: "POST /cohere-chat HTTP/2.0", host: "my-service.10-214-154-169.nip.io", request_id: "de29186609a64b43d452be6a074b451c"
```