Kong GatewayをKubernetes上でHybridモードで構築する（cert-manager使用）

Kong Gatewayにはデプロイ方法が2種類用意されており、ControlPlane(CP)とDataPlane(DP)を分離せず一緒くたにデプロイするTraditionalモードと、ControlPlaneとDataPlaneを別々に展開するHybridモードがある。
冗長構成などを取りたい場合はHybridモードが推奨されており、Kubernetesで利用する場合は同一のHelmリポジトリを利用して`values.yaml`でモードを指定して別々のリリースとしてデプロイしてあげる必要がある。

このHybridモードをHelmで試そうとした時、かなりハマったので手順を残しておく。

なお、デプロイに使うHelmの`values.yaml`は以下にサンプルが大量にあるので、以下を参考にするのもオススメとなる。
https://github.com/Kong/charts/tree/main/charts/kong/example-values

今回は[hybrid-cert-manager](https://github.com/Kong/charts/tree/main/charts/kong/example-values/hybrid-cert-manager)をベースに`values.yaml`を作成した。

## 前提
以下の環境で検証を行った。
- EKS
- cert-managerは導入済み

また、Kong GatewayのデプロイはDBありのHybridモードで行い、証明書は自己署名証明書を使い、管理はcert-managerに任せる。
Kong GatewayへのアクセスはKong Ingress Controller(KIC)経由で行いたいため、Gatewayデプロイ時にあわせてKICもデプロイする。
証明書については[sharedモード](https://docs.konghq.com/gateway/latest/production/deployment-topologies/hybrid-mode/setup/#generate-a-certificatekey-pair)で配布し、クラスタ用に作成した証明書を別のコンポーネントでも使い回すようにする。
なお、HybridモードだとDBが必須となる。
DBがないとCP起動時に`in-memory storage can not be used when role`というエラーになるので注意。


## 構築手順
### 事前準備
最初に`kong`という名前のNamespaceを作成する。
```sh
kubectl create namespace kong
```
次に各コンポーネントで利用する証明書を発行するためのcert-managerのリソースを作成する。
```yaml
cat <<EOF > ./cert.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned-issuer
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: my-selfsigned-ca
  namespace: kong
spec:
  isCA: true
  commonName: my-selfsigned-ca
  secretName: root-secret
  privateKey:
    algorithm: ECDSA
    size: 256
  issuerRef:
    name: selfsigned-issuer
    kind: ClusterIssuer
    group: cert-manager.io
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: my-ca-issuer
  namespace: kong
spec:
  ca:
    secretName: root-secret
EOF
kubectl apply -f ./cert.yaml
```
自分がハマった点として、`kind: ClusterIssuer`だけで済まそうとやったらDataPlaneのデプロイ時に以下のようなエラーが出てデプロイに失敗した点がある。
```sh
2024/11/12 23:57:00 [error] 1336#0: *153 [lua] data_plane.lua:158: communicate(): [clustering] connection to control plane wss://kong-cp-kong-cluster.kong.svc.cluster.local:8005/v1/outlet?node_id=f40db4df-7de3-4e6c-9465-f9c202ad5ef8&node_hostname=kong-dp-kong-db6b76cfc-hpsjx&node_version=3.7.1 broken: ssl handshake failed: 18: self-signed certificate (retrying after 10 seconds) [kong-cp-kong-cluster.kong.svc.cluster.local:8005], context: ngx.timer
```
どうもCA証明書が必須のようで、`kind: Certificate`の`isCA: true`がないとこれが出る模様。
cert-managerの設定を変更する場合は上記の点には気をつけた方がいい。

次にControlPlaneとDataPlaneを作成していくが、その前準備として環境変数を設定する。
```sh
# ControlPlaneのHelmのリリース名
CP_RELEASE_NAME=kong-aws-cp
# DataPlaneのHelmのリリース名
DP_RELEASE_NAME=kong-aws-dp
# Ingressでアクセスする際のドメイン
DOMAIN=eks.hogehoge.info
```

### ControlPlaneのデプロイ
ControlPlaneの`values.yaml`を作成する。
```yaml
cat <<EOF > ./cp.yaml
image:
  repository: kong/kong-gateway
env:
  role: control_plane
  cluster_telemetry_listen: 0.0.0.0:8006
  database: "postgres"
  pg_database: kong
  pg_host: ${CP_RELEASE_NAME}-postgresql.kong.svc
  pg_user: kong
certificates:
  enabled: true
  issuer: "my-ca-issuer"
  cluster:
    enabled: true
    commonName: "$DOMAIN"
    dnsNames:
    - "*.$DOMAIN"
ingressController:
  enabled: true
  env:
    kong_admin_filter_tag: ingress_controller_default
    kong_admin_tls_skip_verify: true
    kong_admin_url: https://localhost:8444
    kong_workspace: default
    publish_service: kong/${DP_RELEASE_NAME}-kong-proxy
  ingressClass: kong
  installCRDs: false
postgresql:
  enabled: true
  auth:
    database: kong
    username: kong
migrations:
  enabled: true
  postUpgrade: true
  preUpgrade: true
manager:
  enabled: true
  type: ClusterIP
  annotations:
    konghq.com/protocol: https
  ingress:
    enabled: true
    ingressClassName: kong
    tls: ${CP_RELEASE_NAME}-kong-cluster-cert
    hostname: kong.$DOMAIN
admin:
  annotations:
    konghq.com/protocol: https
  enabled: true
  type: ClusterIP
  ingress:
    annotations:
      konghq.com/https-redirect-status-code: "301"
      konghq.com/protocols: https
      konghq.com/strip-path: "true"
    enabled: true
    ingressClassName: kong
    tls: ${CP_RELEASE_NAME}-kong-cluster-cert
    hostname: kong.$DOMAIN
    path: /api
enterprise:
  enabled: true
cluster:
  enabled: true
  tls:
    enabled: true
    servicePort: 8005
    containerPort: 8005
clustertelemetry:
  enabled: true
  tls:
    enabled: true
    servicePort: 8006
    containerPort: 8006
proxy:
  enabled: false
portal:
  enabled: false
portalapi:
  enabled: false
status:
  enabled: true
  http:
    enabled: true
    containerPort: 8100
EOF
```
設定内容について、各ブロックを解説する。

```yaml
image:
  repository: kong/kong-gateway
```
上記は`kong/kong-gateway`イメージを使うことでOSS版ではなくEnterprise版のイメージを使うことを明示している。

```yaml
env:
  role: control_plane
  cluster_telemetry_listen: 0.0.0.0:8006
  database: "postgres"
  pg_database: kong
  pg_host: ${CP_RELEASE_NAME}-postgresql.kong.svc
  pg_user: kong
```
上記はKongのパラメータを指定する。
`role`に`control_plane`を指定することでHybridモード、かつデプロイするKongはControlPlaneとして動作することを指示している。
また`database: "postgres"`でDBありモードでデプロイすることを指示している。

```yaml
certificates:
  enabled: true
  issuer: "my-ca-issuer"
  cluster:
    enabled: true
    commonName: "$DOMAIN"
    dnsNames:
    - "*.$DOMAIN"
```
上記は証明書をcert-managerに管理させる場合に指定する項目となる。
`issuer`にcert-managerのIssuerを指定し、`cluster.enabled`でクラスタに対する証明書を発行している。
ここは`cluster`以外にも`admin`や`manager`などの各コンポーネントごとに証明書の設定を書くことが出来るが、sharedモードで動かす場合は証明書は1つで良いので`cluster`のみ指定している。

```yaml
ingressController:
  enabled: true
  env:
    kong_admin_filter_tag: ingress_controller_default
    kong_admin_tls_skip_verify: true
    kong_admin_url: https://localhost:8444
    kong_workspace: default
    publish_service: kong/${DP_RELEASE_NAME}
  ingressClass: kong
  installCRDs: false
```
上記はKICをあわせてデプロイするかどうかの設定であり、今回はありでデプロイしている。
ポイントとしては`publish_service: kong/${DP_RELEASE_NAME}`で、どのDPのプロキシを介してKICを利用するかを設定する箇所であり、ここにDPのProxyのServiceを指定する。
今回のデプロイ順序はCP→DPであり、CPのデプロイ直後はDPがデプロイされておらず、Ingressはこの`publish_service`のServiceが見つからず機能しない。
DPをデプロイした後に初めてIngressは使えるようになる。

`installCRDs: false`については人によっては`true`にした方がいいかもしれない。
（自分の環境は既に入っていたようで、`true`にするとエラーが出る）

```yaml
postgresql:
  enabled: true
  auth:
    database: kong
    username: kong
migrations:
  enabled: true
  postUpgrade: true
  preUpgrade: true
```
上記はPostgresをあわせてデプロイして初期化する設定となる。
Postgresを別で用意する場合は`false`で問題ない。

```yaml
manager:
  enabled: true
  type: ClusterIP
  annotations:
    konghq.com/protocol: https
  ingress:
    enabled: true
    ingressClassName: kong
    tls: ${CP_RELEASE_NAME}-kong-cluster-cert
    hostname: kong.$DOMAIN
```
上記はKong Managerのデプロイ設定となる。
ポイントとしてはIngressで使う証明書を`tls: ${CP_RELEASE_NAME}-kong-cluster-cert`と指定しており、これはクラスタ向けにcert-managaerが払い出す証明書を流用している。

```yaml
admin:
  annotations:
    konghq.com/protocol: https
  enabled: true
  type: ClusterIP
  ingress:
    annotations:
      konghq.com/https-redirect-status-code: "301"
      konghq.com/protocols: https
      konghq.com/strip-path: "true"
    enabled: true
    ingressClassName: kong
    tls: ${CP_RELEASE_NAME}-kong-cluster-cert
    hostname: kong.$DOMAIN
    path: /api
```
上記はAdminAPIのデプロイ設定となる。
こちらもKong Managerと同様に証明書を流用している。
AdminAPIについては注意点がいくつかある。
まず、KongManagerアクセス時、AdminAPIを呼び出す関係でドメインが異なるとクロスオリジン的なものになり、自己証明書だとAdminAPIへのアクセスに失敗してWorkspacesの表示が`"Data cannot be displayed due to an error."`となって表示できない。
そのため、ここではドメインをManagerと統一している。
（設定次第で回避できるっぽいけど、ここでは割愛）
また、ドメインを統一した都合上、パスでアクセス先を変える必要があり、`path: /api`を指定し、更に使い勝手を同じようにするためにKICのAnnotationに`konghq.com/strip-path: "true"`を追加してAdminAPIアクセス時にAdminAPI側に`/api`が渡らないようにしている。



```yaml
enterprise:
  enabled: true
```
上記はEnterprise版の機能を利用する際に有効化する。
使わない場合は`false`でも問題ない。


```yaml
cluster:
  enabled: true
  tls:
    enabled: true
    servicePort: 8005
    containerPort: 8005
clustertelemetry:
  enabled: true
  tls:
    enabled: true
    servicePort: 8006
    containerPort: 8006
```
上記はHybridモード固有で、このCPに繋ぐためのエンドポイント公開のために必要な設定となる。
`clustertelemetry`は無効化しても問題ない。

```yaml
proxy:
  enabled: false
```
ProxyはDP側で有効化するため明示的に`false`にする。
```yaml
portal:
  enabled: false
portalapi:
  enabled: false
status:
  enabled: true
  http:
    enabled: true
    containerPort: 8100
```
その他、不要そうなものは明示的に`false`にした。
statusについてはReadiness/Liveness Proveで利用しており、これを無効化するとPodが`1/2`のままで止まってしまうので有効化している。
（サンプルを見ているとなくても`1/2`にならないようなので、設定次第では不要かもしれない）

ControlPlaneをデプロイする。
```sh
helm upgrade -i $CP_RELEASE_NAME kong/kong -f ./cp.yaml -n kong --wait
```

### DataPlaneのデプロイ
DataPlaneのvalues.yamlを作成する。

```yaml
cat <<EOF > ./dp.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
  http:
    enabled: true
  ingress:
    enabled: false
  tls:
    enabled: true
  type: LoadBalancer
ingressController:
  enabled: false
postgresql:
  enabled: false
migrations:
  enabled: false
manager:
  enabled: false
admin:
  enabled: false
EOF
```
こちらでも各ブロックを解説する。

```yaml
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
```
HybridモードでDataPlaneをデプロイする場合は`role`に`data_plane`を指定する。
また`cluster_control_plane`は必須で、`cluster_telemetry_endpoint`はなくても動きそうだがエラーが出るので先程のControlPlaneのServiceを宛先として指定する。

```yaml
cluster:
  enabled: true
  tls:
    enabled: true

certificates:
  enabled: true
  issuer: "my-ca-issuer"
  cluster:
    enabled: true
    commonName: "eks.$DOMAIN"
    dnsNames:
    - "*.$DOMAIN"
```
ControlPlaneと同じように証明書を作成する。

```yaml
proxy:
  enabled: true
  http:
    enabled: true
  ingress:
    enabled: false
  tls:
    enabled: true
  type: LoadBalancer
```
Proxyの設定になるが、ここではhttp,https両方でアクセスできるようにし、かつ`type: LoadBalancer`でProxyを外部に公開する。
KICはこの公開されたアドレスを使ってIngress処理を行う。

こちらも`helm`コマンドで以下のようにデプロイする。
```sh
helm upgrade -i kong-aws-dp kong/kong -f ./dp.yaml -n kong
```

## 動作確認
DPのデプロイ後、必要に応じて`type: LoadBalancer`で公開されたアドレスをDNS登録する。
変更が浸透したらアクセスしてみる。
```sh
curl kong.$DOMAIN/api -L -k
```
問題なければAdminAPIが出力したJSONが表示されるはずだ。

Kong Managerにもアクセスしてみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/522cee0a-d85b-c9c9-e20d-b77bf3d2d2d8.png)

問題なく表示された。
なお、最初からServiceなどが複数あるのはKICが入っていて、勝手にGatewayに追加してくれてるからである。

ということで、ドキュメントだけで`values.yaml`を書くのは大変なので、Hybridモードで構築しようとする人の助けになれば幸いである。

## 参考文献
- [パラメータのリファレンスのHybridモードに関する箇所](https://docs.konghq.com/gateway/latest/reference/configuration/#hybrid-mode-section)
- [HelmのReadmeのHybridモードに関する箇所](https://github.com/Kong/charts/blob/main/charts/kong/README.md#hybrid-mode)
- [cert-managerでのHybridモードのサンプル](https://github.com/Kong/charts/blob/main/charts/kong/example-values/README.md)