Kong Gateway Operatorを使ってKong KonnectをKubernetesのリソースとして管理する

本記事は「Kong Advent Calendar 2024」の13日目のエントリとして、Kong Gateway OperatorをKonnect向けに使う方法について解説する。

今年の9月にあったAPI Summitで[Kong Konnect](https://docs.jp.konghq.com/konnect/)をKubernetesのCRDで管理する、というネタが[発表された](https://konghq.com/blog/product-releases/managing-konnect-with-kubernetes-crds)。
発表時点ではリリースされていなかったものの、Kong Gateway Operatorの[v1.4.0リリース](https://github.com/Kong/gateway-operator/blob/main/CHANGELOG.md#v140)でKonnectが正式に対応となった。
今回はこれを使ってみる。

## Kong Gateway Operatorとは
そもそもKong Gateway Operator(KGO)自体の知名度が低いので、これの説明を少しだけ。
Kong Ingress Controller(KIC)ではIngress経由でKong Gatewayを操作し、Pluginなどもカスタムリソースとして扱うことが出来るが、KGOはそれよりも広くControl PlaneやData Planeなどもカスタムリソースとして定義して扱うことが出来る。
またAI周りの機能は`kind: AIGateway`というリソースで個別に力を入れていたりする。
`kind: AIGateway`を使ったケースについては以前「[Kong Gateway Operatorのkind: AIGatewayを試す](https://qiita.com/ipppppei/items/91aa00d1650ab5752d8f)」で取り上げたので興味があれば見て欲しい。

記事執筆時のKGOのバージョンは1.4.1が最新版だが、[Reference](https://docs.konghq.com/gateway-operator/1.4.x/reference/custom-resources/)を見ると以下の43個のカスタムリソースが提供されているようである。多すぎ。
- `KongConsumer`
- `KongPlugin`
- `KongClusterPlugin`
- `KongIngressRoute`
- `KongPlugin`
- `KongCACertificate`
- `KongCertificate`
- `KongCredentialACL`
- `KongCredentialAPIKey`
- `KongCredentialBasicAuth`
- `KongCredentialHMAC`
- `KongCredentialJWT`
- `KongDataPlaneClientCertificate`
- `KongKey`
- `KongKeySet`
- `KongLicense`
- `KongPluginBinding`
- `KongRoute`
- `KongSNI`
- `KongService`
- `KongTarget`
- `KongUpstream`
- `KongVault`
- `KongCACertificateSpec`
- `KongCertificateSpec`
- `KongDataPlaneClientCertificateSpec`
- `KongKeySetSpec`
- `KongKeySpec`
- `KongPluginBindingSpec`
- `KongRouteSpec`
- `KongServiceSpec`
- `KongUpstreamSpec`
- `KongVaultSpec`
- `KongConsumerGroup`
- `AIGateway`
- `DataPlaneMetricsExtension`
- `KongPluginInstallation`
- `KonnectExtension`
- `ControlPlane`
- `DataPlane`
- `GatewayConfiguration`
- `KonnectAPIAuthConfiguration`
- `KonnectGatewayControlPlane`


なお、カスタムリソースの説明はリソース内に実装されているようなので、困ったら`kubectl explain kongservices.spec`のように`kubectl explain`を使って掘り下げていけばどういう設定が出来るかは把握出来ると思う。

## 検証
以下の流れで実施する。
1. KGOのインストール
2. Control Planeの作成
3. Data Planeの作成
4. 動作確認

なお、今回は以下の方針で検証する。
- CRDが用意されているものはなるべく使う
- ない場合でもAPIでKonnectを設定し、GUIは極力使わない
- ただし、確認系は分かりやすさを優先してGUIを使う

### KGOのインストール
今回はEKS上にKGOを入れて動作確認する。
[インストールのページ](https://docs.konghq.com/gateway-operator/1.4.x/get-started/konnect/install/)に従ってKGOをインストールする。
最初にリポジトリを追加する。
```sh
helm repo add kong https://charts.konghq.com
helm repo update kong
```
helmコマンドを使ってインストールする。
values.yamlについては[こちら](https://github.com/Kong/charts/blob/main/charts/gateway-operator/values.yaml)を参考にすることでいくつか設定が出来そうだが、ここではお試しということでドキュメントのコマンドそのままでインストールする。
```sh
helm upgrade --install kgo kong/gateway-operator -n kong-system --create-namespace --set image.tag=1.4 \
 --set kubernetes-configuration-crds.enabled=true \
 --set env.ENABLE_CONTROLLER_KONNECT=true
```

### Control Planeの作成
最初にKubernetesクラスタからKonnectを操作するために、Konnectのトークンを登録する。
Konnectのトークンを環境変数に設定する。
```sh
KONNECT_TOKEN=kpat_xxxx
```
トークンを登録する。トークンの管理はSecretではなく`KonnectAPIAuthConfiguration`という専用のカスタムリソースを利用する。（[仕様](https://docs.konghq.com/gateway-operator/1.4.x/reference/custom-resources/#konnectapiauthconfigurationspec)的にはSecretも参照できると思う）
```yaml
cat <<EOF > ./auth.yaml
kind: KonnectAPIAuthConfiguration
apiVersion: konnect.konghq.com/v1alpha1
metadata:
  name: konnect-api-auth
  namespace: default
spec:
  type: token
  token: $KONNECT_TOKEN
  serverURL: us.api.konghq.com
EOF
kubectl apply -f ./auth.yaml
```
なお、作成後、作成したリソースを確認するとトークンが有効かどうか確認できる。
```sh
$ kubectl get konnectapiauthconfigurations.konnect.konghq.com konnect-api-auth
NAME               VALID   ORGID                                  SERVERURL
konnect-api-auth   True    bbe3ced9-475c-4b55-8ea6-07018dxxxxxx   https://us.api.konghq.com
```
次にKonnect上にControl Planeを作成する。
```yaml
cat <<EOF > ./cp.yaml
kind: KonnectGatewayControlPlane
apiVersion: konnect.konghq.com/v1alpha1
metadata:
  name: gateway-control-plane
  namespace: default
spec:
  name: konnect-cp-crd 
  konnect:
    authRef:
      name: konnect-api-auth 
EOF
kubectl apply -f ./cp.yaml      
```
ポイントとしては`spec.konnect.authRef.name`に先ほど作ったAPIのAuthのリソース名を指定するのと、扱うリソースは全てNamespace単位で必要なリソースでありNamespaceを跨がない点は注意。
Applyするとkubectlから確認できる。
```
$ kubectl get konnectgatewaycontrolplanes
NAME                    PROGRAMMED   ID                                     ORGID
gateway-control-plane   True         707452a2-955d-4c1e-8c6c-52517b9117c2   bbe3ced9-475c-4b55-8ea6-07018dxxxxxx
```
APIを使って操作する人的にはIDが分かるのは結構便利。
KonnectのUIからも確認できる。
![20241206145819.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/76c2d74f-531b-1050-481f-e25597bd7dc7.png)
  

### Data Planeの作成

Control PlaneとData Planeの通信に利用するための自己署名証明書を作成する。
```sh
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -sha256 -days 365 \
  -out ca.crt \
  -subj "/C=US/ST=State/L=City/O=Organization/OU=Department/CN=Kong-CA"
openssl genrsa -out dp.key 2048
openssl req -new -key dp.key -out dp.csr \
  -subj "/C=US/ST=State/L=City/O=Organization/OU=Department/CN=data-plane.kong.local"
openssl x509 -req -in dp.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out dp.crt -days 365 -sha256
```
Control Planeが管理するData Planeの証明書に作成した証明書を追加する。
```yaml
cat <<EOF > dpcert.yaml
kind: KongDataPlaneClientCertificate
apiVersion: configuration.konghq.com/v1alpha1
metadata:
  name: dp-cert
  namespace: default
spec:
  controlPlaneRef:
    type: konnectNamespacedRef
    konnectNamespacedRef:
      name: konnect-cp-crd 
  cert: |
$(sed 's/^/    /' dp.crt)
EOF
kubectl apply -f ./dpcert.yaml
```
`spec.controlPlaneRef.konnectNamespacedRef.name`にはControl Planeの名前ではなく`kind: KonnectGatewayControlPlane`で作成したリソース名を指定する。
問題なければ状態取得すると`PROGRAMMED`が`True`となる。
```sh
$ kubectl get konnectgatewaycontrolplanes.konnect.konghq.com
NAME                    PROGRAMMED   ID                                     ORGID
gateway-control-plane   True         707452a2-955d-4c1e-8c6c-52517b9117c2   bbe3ced9-475c-4b55-8ea6-07018dxxxxxx
```
KonnectのWeb画面で確認する場合、`Overview`->`Actions`->`Data Plane Certificates`から確認できる。
![20241206161408.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/f22a3b40-d323-9a54-0255-ff260b605f24.png)

証明書はData Plane側が参照する場合はSecretで利用するため、Secretとしても登録する。
```sh
kubectl create secret tls konnect-client-tls --cert=./dp.crt --key=./dp.key
```

次にData Planeの設定ファイルを作成する。
Konnect向けData Planeの設定は`kind: KonnectExtension`で設定できるので、これを作成する。
```yaml
CP_ID=$(kubectl get konnectgatewaycontrolplanes gateway-control-plane -o jsonpath={.status.id})
CP_ENDPOINT_ID=$(curl -s -H "Authorization: Bearer ${KONNECT_TOKEN}" -XGET https://us.api.konghq.com/v2/control-planes/$CP_ID | jq -r '.config.control_plane_endpoint' | sed -E 's|https://([^.]+).*|\1|')
9e7f0b8993
cat <<EOF > ./dp-extension.yaml
 kind: KonnectExtension
 apiVersion: gateway-operator.konghq.com/v1alpha1
 metadata:
   name: konnect-config
   namespace: default
 spec:
   controlPlaneRef:
     type: konnectID
     konnectID: $CP_ENDPOINT_ID
   controlPlaneRegion: us
   serverHostname: konghq.com
   konnectControlPlaneAPIAuthConfiguration:
     clusterCertificateSecretRef:
       name: konnect-client-tls
EOF
kubectl apply -f ./dp-extension.yaml
```
設定内容としては以下となる。
- `controlPlaneRef`でControl Planeを指定
- `controlPlaneRegion`、`serverHostname`で接続先を指定
- `clusterCertificateSecretRef`で証明書のSecretを指定

ControlPlaneの指定が特殊なので少し補足する。
環境変数`CP_ID`は先程作成した`KonnectGatewayControlPlanes`からControl PlaneのIDを引っ張ってきている。
この後の`CP_ENDPOINT_ID`が曲者で、Control Plane固有のエンドポイントに含まれるIDをYAMLに書く必要があるのだが、これをWebUIからは取得できない。
（正確には取れなくはないのだが、Data Planeをデプロイ直前まで進めないと取得できない。）
なので、これはAPIで取得する。
`https://<リージョン>.api.konghq.com/v2/control-planes/<CP_ID>`にアクセスすると以下のような感じで取得できるので、この`control_plane_endpoint`からID的な部分をsedで抽出して設定したのが`CP_ENDPOINT_ID`となる。
```json
：（省略）
  "config": {
    "control_plane_endpoint": "https://b89939e7f0.us.cp0.konghq.com",
    "telemetry_endpoint": "https://b89939e7f0.us.tp0.konghq.com",
    "cluster_type": "CLUSTER_TYPE_CONTROL_PLANE",
    "auth_type": "pinned_client_certs",
    "cloud_gateway": false,
    "proxy_urls": []
  },
：（省略）
```
あと`controlPlaneRef`は`KonnectGatewayControlPlane`が指定できても良さそうだが、現時点では対応していないため、上記のような手順が必要となる。

**※'25/6/12追記：今だとKonnectGatewayControlPlaneが指定でき、エンドポイントの指定も不要となった**

次にData Planeをデプロイする。
```yaml
cat <<EOF > ./dp.yaml
 apiVersion: gateway-operator.konghq.com/v1beta1
 kind: DataPlane
 metadata:
   name: dataplane-example
   namespace: default
 spec:
   extensions:
   - kind: KonnectExtension
     name: konnect-config
     group: gateway-operator.konghq.com
   deployment:
     podTemplateSpec:
       spec:
         containers:
         - name: proxy
           image: kong/kong-gateway:3.8.1.0
           env:
           - name: KONG_LOG_LEVEL
             value: debug
EOF
kubectl apply -f dp.yaml
```
先ほど作成した`KonnectExtension`は`spec.extensions`に作成したリソース名を指定して渡す。
リソース作成後、`kubectl get dataplane`で適用されたことは確認できるが、Konnectと接続できていなくてもここの`READY`は`True`となる。
なのでここはKonnectのUIから確認するのが手っ取り早い。
以下のような感じでData Planeが見えれば問題なくデプロイできている。
![20241209105421.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/f859d167-9aec-a72f-f3a0-d3dac602d46e.png)
 

### 動作確認
Kong Gateway内にServiceとRouteを作成してData Plane経由でアクセスし、問題なく動作するか確認する。
なお、ServiceとRouteは以下の内容で作成する。
- Service：
  - 宛先：`https://httpbin.org`
- Route：
  - パス：`/httpbin`

なお、ServiceとRouteはKong Ingress Controllerで作成せず、`kind: KongService`と`kind: KongRoute`を使って作成する。
最初に`KongService`を作成する。
```yaml
cat <<EOF > ./kong-svc.yaml
kind: KongService
apiVersion: configuration.konghq.com/v1alpha1
metadata:
  name: httpbin-svc
  namespace: default
spec:
  name: httpbin-svc
  host: httpbin.org
  protocol: https
  port: 443
  controlPlaneRef:
    type: konnectNamespacedRef 
    konnectNamespacedRef:
      name: gateway-control-plane
EOF
kubectl apply -f ./kong-svc.yaml
```
`spec.controlPlaneRef`で作成した`KonnectGatewayControlPlane`のリソース名を指定する。
次にRouteを`kind: KongRoute`を使って作成する。
```yaml
cat <<EOF > ./kong-rt.yaml
kind: KongRoute
apiVersion: configuration.konghq.com/v1alpha1
metadata:
  name: httpbin-rt
  namespace: default
spec:
  name: httpbin-rt
  protocols:
  - https
  paths:
  - /httpbin
  serviceRef:
    type: namespacedRef
    namespacedRef:
      name: httpbin-svc
EOF
kubectl apply -f ./kong-rt.yaml
```
`spec.serviceRef`で作成した`KongService`のリソース名を指定することで、Serviceとの紐づけを行う。

Service、Routeの作成が終わったらData Planeを通してhttpbin.orgにアクセスする。
Data PlaneをデプロイするとProxyが`type: LoadBalancer`で公開されるので、これを利用する。
```sh
$ kubectl get svc -l konghq.com/gateway-operator=dataplane
NAME                                        TYPE           CLUSTER-IP       EXTERNAL-IP                                                               PORT(S)                      AGE
dataplane-admin-dataplane-example-sp9j4     ClusterIP      None             <none>                                                                    8444/TCP                     8m51s
dataplane-ingress-dataplane-example-79stt   LoadBalancer   10.100.156.238   a26783be2b8bf40ccb795094690efb6a-12968xxxxx.us-east-1.elb.amazonaws.com   80:32504/TCP,443:30441/TCP   8m51s
```
Proxyのアドレスを環境変数に設定する。
```sh
PROXY=$(kubectl get svc -l gateway-operator.konghq.com/dataplane-service-type=ingress -o jsonpath={.items[].status.loadBalancer.ingress[].hostname})
```
アクセスする。
```json
$ curl -k -s -XGET https://${PROXY}/httpbin/anything
{
  "user-agent": "curl/8.7.1"
}
```
問題なく取得できた。

ついでにUIで設定を変更した場合、どのように動作するかも確認する。
期待としては、Manifest管理する以上はKubernetes側のルールに沿ってKonnect側の設定が元に戻って欲しい。
今は以下のように表示される。
```sh
$ kubectl get kongservice,kongroute
NAME                                               HOST          PROTOCOL   PROGRAMMED
kongservice.configuration.konghq.com/httpbin-svc   httpbin.org              True

NAME                                            PROGRAMMED
kongroute.configuration.konghq.com/httpbin-rt   True
```
UI上でホストを`httpbin.kongh.com`に変更する。
![20241209112453.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/4b9b17de-0479-128f-d7b6-df445f5cde1e.png)

設定後、暫く待つとKonnect側の表示が元に戻った。
![20241209113357.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/79255f6b-f46c-0ee1-ce7f-6c13135835c3.png)
  
期待通りリコンサイルが動作し、Kubernetes側の設定が優先された。

## まとめ
Konnect向けKGO機能を構築から疎通確認まで一通り触ってみたが、普通に使えるな、というのが個人的な感想である。
Konnect側の設定をManifestで展開できるので、構築の自動化とかを検討する人には向いていると思う。
また、[Kustomize](https://kustomize.io/)や[ytt](https://carvel.dev/ytt/)を使えばGitOps的なアプローチで任意の環境の構築自動化も簡単に行えると思う。
あと、上記検証では触れなかったが、K8sによるリコンサイルはあくまでもManifestで管理している対象にのみ効き、Manifestで管理していないService、Routeは削除されない。
なので、KonnectのUIも表示専用という訳ではなく、Manifest外のエンティティは作成・変更・削除等利用できる。
この点は先日「[KonnectでKong Ingress Controllerを使う](https://qiita.com/ipppppei/items/ca3bb324921fa7ee6054)」で紹介したKICモードよりも使い勝手がいい。
Kubernetes Firstなシステム、プロジェクトには相性がいいと思うので、KonnectやKong Gatewayを使うKubernetesユーザは試していただければ幸いである。

## おまけ
以下のようなYAMLを用意するとKIC向けのKonnectのControl Planeも作成できる。
```yaml
kind: KonnectGatewayControlPlane
apiVersion: konnect.konghq.com/v1alpha1
metadata:
  name: gateway-control-plane-kic
  namespace: default
spec:
  name: konnect-cp-crd-kic
  konnect:
    authRef:
      name: konnect-api-auth
  cluster_type: CLUSTER_TYPE_K8S_INGRESS_CONTROLLER
```
ただし、`kind: KongService`を作っても以下のような感じでエラーとなる。
```sh
    Message:               Attaching to ControlPlane default/gateway-control-plane-kic with cluster type CLUSTER_TYPE_K8S_INGRESS_CONTROLLER is not supported
    Observed Generation:   1
    Reason:                Invalid
    Status:                False
    Type:                  ControlPlaneRefValid
```
KICを使う場合はKICのルールに則ってKong GatewayのService/Routeを作る必要がありそうだ。