KonnectのDataPlaneをPrometheus/Grafanaで監視する

本記事は「Kong Advent Calendar 2024」の21日目のエントリとして、KonnectのDataPlaneをPrometheus/Grafanaで監視する方法について解説する。

基本的には以下のための手順を整理したメモとなる。
- KonnectのDataPlaneのメトリクス取得
- DataPlaneがProxyする通信のメトリクス取得

メトリクス取得にはPrometheusを利用し、可視化はGrafanaを利用する。
今回はKonnect向けに書いているが設定はKong Gatewayにも流用可能なので、Kong Gatewayでも同じようなことをしたい人には参考になると思う。

## 前提
以下の前提で構築および検証を実施する。
- DataPlaneとPrometheusの展開先にEKSを利用
- [cert-manager](https://cert-manager.io/)は導入済み
- Prometheus/Grafanaの構築には[Prometheus Operator](https://github.com/prometheus-operator/prometheus-operator)を利用
- Prometheus/Grafanaの公開に使うIngress Controllerは[Contour](https://github.com/projectcontour/contour)を使用
- KonnectのControlPlaneは作成済み

Ingress Controllerは当初Kong Ingress Controller(KIC)を利用しようと考えていたが、Konnectでの利用はUIがRead-Onlyになるなど少し制約が面倒だったので今回はContourにした。（参考：[Kong Ingress Controller for Kubernetes Association](https://docs.konghq.com/konnect/gateway-manager/kic/)）


## 構築
### Ingress Controller(Contour)の構築
[この辺](https://projectcontour.io/getting-started/#option-2-helm)の手順に従ってインストールする。
Helmのリポジトリを追加する。
```sh
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
```
インストールする。環境にもよるが大体1分くらいで完了する。
```sh
helm upgrade -i contour bitnami/contour -n projectcontour --create-namespace --wait 
```
設定値を変更したい人は[こちら](https://artifacthub.io/packages/helm/bitnami/contour?modal=values)からvalues.yamlの値が参照できる。

インストールすると以下のIngressClassが確認できる。
```sh
$ kubectl get ingressclass contour
NAME      CONTROLLER                                         PARAMETERS   AGE
contour   projectcontour.io/projectcontour/contour-contour   <none>       5m56s
```
今後はこれを使ってIngressを作成する。

### Prometheus/Grafanaの構築
PrometheusとGrafanaはPrometheus Operatorを使って構築する。
構築方法は[Helmのリポジトリ](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack)に記載があるのでこれを参考にインストールする。

リポジトリを追加する。
```sh
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
```
`values.yaml`を作成する前に、Ingressで利用するドメインを環境変数に設定する。
```sh
DOMAIN=eks.hogehoge.info
```
`values.yaml`を作成する。
```yaml
cat <<EOF > ./prometheus-stack-values.yaml
alertmanager:
  ingress:
    enabled: true
    ingressClassName: contour
    annotations:
      cert-manager.io/issuer: prometheus-stack-kube-prom-self-signed-issuer
    hosts:
    - alertmanager.$DOMAIN
    tls:
    - secretName: alertmanager-general-tls
      hosts:
      - alertmanager.$DOMAIN
grafana:
  adminPassword: admin
  ingress:
    enabled: true
    ingressClassName: contour
    annotations:
      cert-manager.io/issuer: prometheus-stack-kube-prom-self-signed-issuer
    hosts:
    - grafana.$DOMAIN
    tls:
    - secretName: grafana-general-tls
      hosts:
      - grafana.$DOMAIN
  persistence:
    enabled: true
    type: statefulset
    accessModes:
    - ReadWriteOnce
    size: 20Gi
    finalizers:
    - kubernetes.io/pvc-protection
prometheusOperator:
  admissionWebhooks:
    certManager:
      enabled: true
prometheus:
  prometheusSpec:
    storageSpec:
      volumeClaimTemplate:
        spec:
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 20Gi
  ingress:
    enabled: true
    ingressClassName: contour
    annotations:
      cert-manager.io/issuer: prometheus-stack-kube-prom-self-signed-issuer
    hosts:
    - prometheus.$DOMAIN
    tls:
    - secretName: prometheus-general-tls
      hosts:
      - prometheus.$DOMAIN
EOF
```
何となく分かると思うので詳細な説明は割愛するが、基本的にはcert-manager向けの設定とドメインの設定、ストレージ容量の指定をしているくらいである。
cert-managerのための`Issuer`や`Certificate`などは自動生成されるので用意する必要はない。

インストールする。
```sh
helm upgrade -i -f prometheus-stack-values.yaml prometheus-stack prometheus-community/kube-prometheus-stack -n prometheus-stack --create-namespace --wait
```
指定したホスト名がIngressの`ADDRESS`に表示されるアドレスを向くようDNSサーバ側で名前解決の設定を入れる。
設定後、PrometheusとGrafanaにアクセスするとそれぞれ画面が確認できる。
![20241121161746.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/29505642-71c6-fd41-27f4-6119963aabec.png)

![20241121161759.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/c2840a61-31de-c8a3-a1d8-c2b60be1bf93.png)

Grafanaの初期ログインユーザ名とパスワードは`<helm release名>-grafana`に格納されている。
今回は`values.yaml`でパスワードに`admin`を設定しており、初期ユーザは`admin`になるので、両方`admin`を指定すればログイン出来る。

### DataPlaneの構築
Konnectと繋ぐためのDataPlaneをデプロイする。
ブラウザからKonnectにログインし、`Data Plane Nodes`->`New Data Plane Node`を選択し、作成画面の`Platform`で`Kubernetes`を選択する。
選択すると以下のように進行手順が表示されるので、これに従ってDataPlaneをデプロイする。
![20241121163729.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/083dddc8-831b-ead1-8cae-faddb5792645.png)

リポジトリを追加する。
```sh
kubectl create namespace kong
helm repo add kong https://charts.konghq.com
helm repo update
```
証明書を生成し、`tls.crt`と`tls.key`という名前で保存する。
保存後にSecretを作成し、DataPlaneが証明書を参照できるようにする。
```sh
kubectl create secret tls kong-cluster-cert -n kong --cert=./tls.crt --key=./tls.key
```
Secretの作成が終わったら、表示されているDataPlaneの`values.yaml`を一旦ファイルに保存する。

#### DataPlaneのIngress設定
ここから一工夫加える。
せっかくIngressがあるので、ProxyをIngressで公開するよう設定を追加する。
通信はIngressでTLS終端してKong GatewayにはHTTPを渡すようにする（ContourだとContourのCRDである`kind: HTTPProxy`を使わないとTLSのパススルーが出来ないっぽい）。
IngressでTLS終端するために自己署名証明書発行のための`Issuer`と`Certificate`を作成しapplyする。
```yaml
cat <<EOF > ./proxy-cert.yaml
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: kong-konnect-dp-issuer-root
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: kong-konnect-dp-ca
spec:
  commonName: kong-konnect-dp-ca
  secretName: kong-konnect-dp-ca
  isCA: true
  issuerRef:
    group: cert-manager.io
    kind: Issuer
    name: kong-konnect-dp-issuer-root
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: kong-konnect-dp-issuer
spec:
  ca:
    secretName: kong-konnect-dp-ca
EOF
kubectl apply -f ./proxy-cert.yaml
```
このIssuerを使って自己署名証明書を作成し、Ingressリソースも作成するために`values.yaml`に対して`proxy`ブロックを追記する。
```yaml
cat <<EOF >> ./values.yaml
proxy:
  enabled: true
  type: ClusterIP
  http:
    enabled: true
  ingress:
    enabled: true
    ingressClassName: contour
    hostname: "proxy.$DOMAIN"
    tls:
    - hosts:
      - proxy.$DOMAIN
      secretName: kong-konnect-dp-general-tls
    annotations:
      cert-manager.io/issuer: kong-konnect-dp-issuer
  tls:
    enabled: false
EOF
```
TLSはIngressでは使うので`proxy.ingress.tls`は設定するが、IngressでTLSを終端させた後はHTTPでProxyにアクセスするため、`proxy.http.enabled`は`true`で、`proxy.tls.enabled`は`false`となる

#### DataPlaneの監視設定
もう少し`values.yaml`に追加設定する。
次はDataPlaneを監視するために`ServiceMonitor`の設定を入れる。
`ServiceMonitor`はPrometheus Operatorが提供するカスタムリソースであり、このリソース内で指定した条件に合致する`kind: Service`で提供されるリソースを自動的にスクレイピングしてくれるものである。
`ServiceMonitor`は幸いなことに`values.yaml`で値を指定すれば自動生成されるので、その設定も追加する。

```yaml
cat <<EOF >> ./values.yaml
serviceMonitor:
  enabled: true
  namespace: prometheus-stack
  labels:
    release: prometheus-stack
EOF
```
`namespace`にPrometheusがいるNamespaceを指定し、Prometheus側に検出してもらうためにラベルを設定している。
なぜこのラベルなのかは「[Prometheus OperatorのServiceMonitor / PodMonitor検出ルール](https://qiita.com/ipppppei/items/d6034c6bed3364f89461)」という内容で以前触れているのでそちらを参照して欲しい。

#### DataPlaneのデプロイ

この`values.yaml`を使ってDataPlaneをデプロイする。
```sh
helm upgrade -i kong-konnect-dp kong/kong -n kong -f ./values.yaml
```
デプロイが完了すると以下のようにDataPlaneが見つかった旨が表示される。
![20241121164522.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/7df8cdb3-a5fc-7659-4a81-86be1df50fb7.png)

作成したIngress経由でProxyにアクセスできることを確認する。
```sh
$ curl https://proxy.$DOMAIN -k
{
  "message":"no Route matched with those values",
  "request_id":"8767816cf10a442aa25ba93f36a83904"
}
```

Prometheus側を見ると、TargetにDataPlaneがいることが分かる。
![20241122124933.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/6d7cc99d-d196-8512-5da3-456ced899fbf.png)

PrometheusのExpression BrowserからKongに関連するメトリクスが取れていることも分かる。
![20241122130254.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/899635bc-5edf-12a5-499a-c0f4b35ffe1b.png)

なお、何度か試していた時に、たまにTargetにDataPlaneが出てこないことがあった。
確認すると`ServiceMonitor`に指定したラベルが反映されていなかった（原因は不明）。
その場合は`kubectl edit servicemonitor -n prometheus-stack kong-konnect-dp-kong`
みたいな感じで`ServiceMonitor`を修正し、`metadata.labels`に`release: prometheus-stack`を追加してあげるとよい。

### Grafanaのダッシュボードの作成
メトリクスが取れていることが確認できたので、ダッシュボードを作成する。
Kong GatewayのダッシュボードはGrafanaLabsで公開されているので、新規に作成せずにここで公開されているものを使ってダッシュボードを作成する。
![20241122130454.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/cea712c3-534e-ae25-68c3-e4b94a68cadf.png)
（図：Grafana Labsで"kong"でダッシュボードを検索した結果）

Grafanaにログインし、左サイドバーの`Dashboards`から`New`->`New dashboard`をクリックする。
次に`Import dashboard`->`Discard`をクリックし、以下の新規にダッシュボードをインポートする設定に移る。
![20241122130837.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/82afb4f9-79b0-8cd5-de08-f98631b039dd.png)

Kong Officialのダッシュボードは[こちら](https://grafana.com/grafana/dashboards/7424-kong-official/)にあり、このIDは7424なので、このIDを入力して`Load`をクリックし、データソースにPrometheusを選択して`Import`をクリックする。
![20241122131100.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/c04400ca-842f-3e54-686e-30cd7bb11f43.png)

Importが完了するとダッシュボードが表示される。
ただし、今は通信を行っておらず、かつPrometheus Pluginも入れていないので見れるメトリクスは限定的であり、ほとんどがNo dataと表示される。
![20241122131337.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/a2881960-0e08-7deb-33fe-6a92c8ea3f64.png)

なお、nginxのような通信とは関係ない部分は確認可能。
![20241122131420.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/3c36cbe4-d69d-ba1b-f3dd-579fcbac44fb.png)

なお、Proxy Podの負荷状態に関するパネルはKongのダッシュボードには用意されていない。
これに関してはPrometheus OperatorでデプロイしたGrafanaに標準でついているダッシュボード（kubernetes-mixin）を使うとCPU・メモリ利用率、帯域使用量などが確認できるので、それで代用するとよい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/a0f6f3f6-ac3c-4634-78c7-88b5c6fb25d3.png)

## DataPlaneがProxyする通信のメトリクス取得
適当に通信を行って、Proxyした通信のメトリクスが見れるかを確認する。
ServiceとRouteをAPIで作成するために、最初にKonnectのトークン、ControlPlaneのID、KonnectのAPIエンドポイントをそれぞれ環境変数に設定する。

```sh
KONNECT_TOKEN=kpat_ER1WQxxxx
CP_ID=788397a0-b6f4-4dc8-a2f8-63cc6f0c62bb
KONNECT_API=https://us.api.konghq.com/v2
```
curlでAPIを発行してService、Routeを作成する。なお叩いているAPIの使用は[この辺](https://docs.konghq.com/konnect/api/control-plane-configuration/latest/#/Services/create-service)を参考にした。
最初にServiceを作成する。
```sh
curl -s -X POST \
  "${KONNECT_API}/control-planes/${CP_ID}/core-entities/services" \
  -H "Authorization: Bearer $KONNECT_TOKEN" \
  -d name=httpbin-service \
  -d url=https://httpbin.konghq.com
```
作成時に表示されるServiceのIDを環境変数に設定する。
```sh
SERVICE_ID=306ed63f-2022-447b-90be-2425cdafa661
```
Routeを作成する。
```sh
curl -s -X POST \
  "${KONNECT_API}/control-planes/${CP_ID}/core-entities/routes" \
  -H "Authorization: Bearer ${KONNECT_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "httpbin-route",
    "paths": ["/httpbin"],
    "service": {
      "id": "'"${SERVICE_ID}"'"
    }
  }'

```
ServiceとRouteの作成が済んだらPrometheus Pluginを設定する。
ここではServiceに対して適用する。
```sh
curl -s -X POST \
  "${KONNECT_API}/control-planes/${CP_ID}/core-entities/services/${SERVICE_ID}/plugins" \
  -H "Authorization: Bearer $KONNECT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "prometheus",
    "config": {
      "status_code_metrics": true,
      "latency_metrics": true,
      "bandwidth_metrics": true,
      "upstream_health_metrics": true
     }
   }'
```
動作確認する。
以下のような感じで一定時間Proxyに対してアクセスする。
```sh
watch curl -s -k https://proxy.$DOMAIN/httpbin/user-agent
```
少し時間が経ったらPrometheusがスクレイピングしてメトリクスが見れるはずだ。
Prometheus側でメトリクスが取れることを確認する。
ここでは一例として、リクエスト数を確認してみる。
![20241122164951.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/51ed9803-bf67-4c1d-eb87-28f7905b9eac.png)

`exported_service="httpbin-service"`に対してのアクセスが`code="200"`を返した回数が15回であることが確認できた。
Grafanaのダッシュボードの方も確認する。

![20241122165242.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/af9a8b10-f4a3-5393-eb54-5ac9816980a6.png)

今度はNo dataとならずRPSやレイテンシがグラフィカルに表示されることが確認できた。

## まとめ
KonnectのDataPlaneの監視方法については公式ドキュメントには特に記載がなかったが、Helmの`values.yaml`を読み解くことで`ServiceMonitor`を使って簡単にPrometheusの監視下に置くことが確認できた。
また公式のダッシュボードにはDataPlaneのパネルはないものの、Prometheus Operator標準のパネルで監視できることも確認できた。
今回アラートは特に設定しなかったが、必要なメトリクスが取れていることも分かったので、DataPlaneの障害時には通知を飛ばすのは難しくなさそうだ。
また、DataPlaneがProxyする通信のメトリクスも可視化できたので、こちらもアラート対象にする見込みを得ることが出来た。