cert-managerとPrometheus OperatorでTLS化したPrometheus/Grafanaを立てる

PrometheusをKubernetes上に構築する際、[Kubernetes Operator](https://github.com/prometheus-operator/prometheus-operator)を使うのがメジャーだと思うが、これは[cert-manager](https://cert-manager.io/)向けの作り込みもされており、cert-managerが導入済みであれば簡単にTLS化して立てることもできる。
今回はそれを試してみる。

なお、前提として既にKubernetes環境があり、`type: LoadBalancer`が使えるものとする。
ベースとなるKubernetesにはvSphere with Tanzu（Kubernetes：v1.25.7）を利用したが、おそらく何を使っても同じ結果が得られるはず。

## 事前準備：IngressControllerとcert-managerの導入
PrometheusとGrafanaをクラスタ外に公開するのにIngressを使い、証明書の管理にcert-managerを使う。
Prometheus OperatorのHelm的にはIngress Controllerは[nginx-ingress](https://github.com/kubernetes/ingress-nginx)を想定した作りになっているが、商用環境でnginx-ingressを使う人は少ないと思うので、ここでは[Contour](https://projectcontour.io/)で進める。

### Contourのインストール
今回はディストリビューション依存をなくすため、Tanzu Packagesは使わずに普通にOSS版のContourをインストールする。
```sh
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install contour bitnami/contour --namespace projectcontour --create-namespace
```
Podが立ち上がってEnvoyのServiceにExternal-IPが割り振られればOK。
```sh
$ kubectl get all -n projectcontour
NAME                                   READY   STATUS    RESTARTS   AGE
pod/contour-contour-64c7959cf9-vk684   1/1     Running   0          91s
pod/contour-envoy-b72ld                2/2     Running   0          91s
pod/contour-envoy-bkxvp                2/2     Running   0          91s
pod/contour-envoy-plspq                2/2     Running   0          91s

NAME                    TYPE           CLUSTER-IP       EXTERNAL-IP      PORT(S)                      AGE
service/contour         ClusterIP      10.102.173.86    <none>           8001/TCP                     91s
service/contour-envoy   LoadBalancer   10.100.171.238   10.214.154.165   80:32533/TCP,443:31826/TCP   91s

NAME                           DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
daemonset.apps/contour-envoy   3         3         3       3            3           <none>          91s

NAME                              READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/contour-contour   1/1     1            1           91s

NAME                                         DESIRED   CURRENT   READY   AGE
replicaset.apps/contour-contour-64c7959cf9   1         1         1       92s
```
また、`IngressClass`も作成される。
```
$ kubectl get ingressclass
NAME      CONTROLLER                                         PARAMETERS   AGE
contour   projectcontour.io/projectcontour/contour-contour   <none>       12m
```


### cert-managerのインストール
cert-managerも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
```
Podが起動してCRDもインストールされていればOK。
```sh
$ kubectl get all -n cert-manager
NAME                                           READY   STATUS    RESTARTS   AGE
pod/cert-manager-578c949894-4t8sg              1/1     Running   0          82s
pod/cert-manager-cainjector-6d94d78587-x9xfv   1/1     Running   0          82s
pod/cert-manager-webhook-7b6dfb95d5-mjgct      1/1     Running   0          82s

NAME                           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/cert-manager           ClusterIP   10.96.235.229   <none>        9402/TCP   82s
service/cert-manager-webhook   ClusterIP   10.97.130.86    <none>        443/TCP    82s

NAME                                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/cert-manager              1/1     1            1           83s
deployment.apps/cert-manager-cainjector   1/1     1            1           83s
deployment.apps/cert-manager-webhook      1/1     1            1           83s

NAME                                                 DESIRED   CURRENT   READY   AGE
replicaset.apps/cert-manager-578c949894              1         1         1       83s
replicaset.apps/cert-manager-cainjector-6d94d78587   1         1         1       83s
replicaset.apps/cert-manager-webhook-7b6dfb95d5      1         1         1       83s
```
```sh
$ kubectl api-resources |grep cert-manager
challenges                                                             acme.cert-manager.io/v1                  true         Challenge
orders                                                                 acme.cert-manager.io/v1                  true         Order
certificaterequests               cr,crs                               cert-manager.io/v1                       true         CertificateRequest
certificates                      cert,certs                           cert-manager.io/v1                       true         Certificate
clusterissuers                                                         cert-manager.io/v1                       false        ClusterIssuer
issuers                                                                cert-manager.io/v1                       true         Issuer
```

## Promethes Operatorのインストール
[`kube-prometheus-stack`](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack)を使ってデプロイする。
まず最初にHelmのリポジトリを追加する。
```sh
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
```
Helmの`values.yaml`を取り出す。
```sh
helm show values prometheus-community/kube-prometheus-stack > prom-values.yaml
```
AlertManager、Prometheus、Grafanaについて`values.yaml`に以下の変更を加える。
- Ingressの有効化
- IngressClassの指定
- ホスト名の設定
- cert-manager使用の有効化
- IngressのAnnotationにcert-managerの自己署名証明書の`Issuer`を指定し証明書を発行する

自己署名証明書用の`Issuer`についてはPrometheus OperatorのHelm内で`<Helmのリリース名>-kube-prom-self-signed-issuer`が作成されるので、これを使って自己署名証明書を発行する。
また、ホスト名にはここではフリーのワイルドカードDNSサービスの[nip.io](https://nip.io/)を使い、`prometheus.10-214-154-165.nip.io`のような感じでContourのEnvoyのExternal-IPを渡すようにしている。
自前のドメインを使う場合はドメインの指す先をEnvoyのExternal-IPにし、そのドメインを`values.yaml`に記載するとよい。

最終的な変更は以下のようになった。
<details><summary>差分（クリックして表示）</summary><div>

```diff:prom-values.yaml
@@ -385,13 +385,14 @@
   #       {{ end }}

   ingress:
-    enabled: false
+    enabled: true

     # For Kubernetes >= 1.18 you should specify the ingress-controller via the field ingressClassName
     # See https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#specifying-the-class-of-an-ingress
-    # ingressClassName: nginx
+    ingressClassName: contour

-    annotations: {}
+    annotations:
+      cert-manager.io/issuer: prometheus-stack-kube-prom-self-signed-issuer

     labels: {}

@@ -403,8 +404,8 @@

     ## Hosts must be provided if Ingress is enabled.
     ##
-    hosts: []
-      # - alertmanager.domain.com
+    hosts:
+    - alertmanager.10-214-154-165.nip.io

     ## Paths to use for ingress rules - one path should match the alertmanagerSpec.routePrefix
     ##
@@ -418,10 +419,10 @@
     ## TLS configuration for Alertmanager Ingress
     ## Secret must be manually created in the namespace
     ##
-    tls: []
-    # - secretName: alertmanager-general-tls
-    #   hosts:
-    #   - alertmanager.example.com
+    tls:
+    - secretName: alertmanager-general-tls
+      hosts:
+      - alertmanager.10-214-154-165.nip.io

   ## Configuration for Alertmanager secret
   ##
@@ -977,16 +978,17 @@
   ingress:
     ## If true, Grafana Ingress will be created
     ##
-    enabled: false
+    enabled: true

     ## IngressClassName for Grafana Ingress.
     ## Should be provided if Ingress is enable.
     ##
-    # ingressClassName: nginx
+    ingressClassName: contour

     ## Annotations for Grafana Ingress
     ##
-    annotations: {}
+    annotations:
+      cert-manager.io/issuer: prometheus-stack-kube-prom-self-signed-issuer
       # kubernetes.io/ingress.class: nginx
       # kubernetes.io/tls-acme: "true"

@@ -999,7 +1001,8 @@
     ##
     # hosts:
     #   - grafana.domain.com
-    hosts: []
+    hosts:
+    - grafana.10-214-154-165.nip.io

     ## Path for grafana ingress
     path: /
@@ -1007,10 +1010,10 @@
     ## TLS configuration for grafana Ingress
     ## Secret must be manually created in the namespace
     ##
-    tls: []
-    # - secretName: grafana-general-tls
-    #   hosts:
-    #   - grafana.example.com
+    tls:
+    - secretName: grafana-general-tls
+      hosts:
+      - grafana.10-214-154-165.nip.io

   # # To make Grafana persistent (Using Statefulset)
   # #
@@ -2457,7 +2460,7 @@

     # Use certmanager to generate webhook certs
     certManager:
-      enabled: false
+      enabled: true
       # self-signed root certificate
       rootCert:
         duration: ""  # default to be 5y
@@ -3136,13 +3139,14 @@
   #     someoneelse:$apr1$DMZX2Z4q$6SbQIfyuLQd.xmo/P0m2c.

   ingress:
-    enabled: false
+    enabled: true

     # For Kubernetes >= 1.18 you should specify the ingress-controller via the field ingressClassName
     # See https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#specifying-the-class-of-an-ingress
-    # ingressClassName: nginx
+    ingressClassName: contour

-    annotations: {}
+    annotations:
+      cert-manager.io/issuer: prometheus-stack-kube-prom-self-signed-issuer
     labels: {}

     ## Redirect ingress to an additional defined port on the service
@@ -3153,7 +3157,8 @@
     ##
     # hosts:
     #   - prometheus.domain.com
-    hosts: []
+    hosts:
+    - prometheus.10-214-154-165.nip.io

     ## Paths to use for ingress rules - one path should match the prometheusSpec.routePrefix
     ##
@@ -3167,10 +3172,10 @@
     ## TLS configuration for Prometheus Ingress
     ## Secret must be manually created in the namespace
     ##
-    tls: []
-      # - secretName: prometheus-general-tls
-      #   hosts:
-      #     - prometheus.example.com
+    tls:
+    - secretName: prometheus-general-tls
+      hosts:
+      - prometheus.10-214-154-165.nip.io

   ## Configuration for creating an Ingress that will map to each Prometheus replica service
   ## prometheus.servicePerReplica must be enabled
```
</div></details>

変更した`values.yaml`を用いてPrometheus/Grafanaをデプロイする。
```sh
helm upgrade -i -f prom-values.yaml prometheus-stack prometheus-community/kube-prometheus-stack -n prometheus-stack --create-namespace
```
デプロイが成功すると、以下のような感じでIngressが作成される。
```sh
$ kubectl get ing -n prometheus-stack
NAME                                      CLASS     HOSTS                                ADDRESS          PORTS     AGE
prometheus-stack-grafana                  contour   grafana.10-214-154-165.nip.io        10.214.154.165   80, 443   106s
prometheus-stack-kube-prom-alertmanager   contour   alertmanager.10-214-154-165.nip.io   10.214.154.165   80, 443   106s
prometheus-stack-kube-prom-prometheus     contour   prometheus.10-214-154-165.nip.io     10.214.154.165   80, 443   106s
```
自己署名証明書の発行のための`Issuer`も作成され、それを元に`Certificate`も作成される。
TLSでのアクセスが上手く行かない場合は`Certificate`が上手く作れているか（`READY`が`True`になっているか）を確認するとよい。
```sh
$ kubectl get issuer,certificate -n prometheus-stack
NAME                                                                   READY   AGE
issuer.cert-manager.io/prometheus-stack-kube-prom-root-issuer          True    50s
issuer.cert-manager.io/prometheus-stack-kube-prom-self-signed-issuer   True    50s

NAME                                                               READY   SECRET                                 AGE
certificate.cert-manager.io/alertmanager-general-tls               True    alertmanager-general-tls               50s
certificate.cert-manager.io/grafana-general-tls                    True    grafana-general-tls                    50s
certificate.cert-manager.io/prometheus-general-tls                 True    prometheus-general-tls                 50s
certificate.cert-manager.io/prometheus-stack-kube-prom-admission   True    prometheus-stack-kube-prom-admission   50s
certificate.cert-manager.io/prometheus-stack-kube-prom-root-cert   True    prometheus-stack-kube-prom-root-cert   50s
```
作成された証明書は以下となる。
```sh
$ kubectl get secret -n prometheus-stack |grep general
alertmanager-general-tls                                            kubernetes.io/tls    3      53m
grafana-general-tls                                                 kubernetes.io/tls    3      53m
prometheus-general-tls                                              kubernetes.io/tls    3      53m
```
適切にSANが設定されているか試しに1つ見てみる。
```sh
kubectl get secret -n prometheus-stack prometheus-general-tls -o jsonpath={.data.'ca\.crt'}  | base64 -d | openssl x509 -noout -text | grep -A 1 "Subject Alternative Name"
```
得られた出力は以下となる。
```sh
            X509v3 Subject Alternative Name: critical
                DNS:prometheus.10-214-154-165.nip.io
```
問題なさそうだ。

## 動作確認
実際にPrometheusにブラウザでアクセスしてみる。
![1710994521158.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/7773151b-0161-331a-0e86-e0a16345bb9d.png)

アクセス自体は問題ない。ただ、targetsを確認したところNode Exporterなどいくつかのターゲットからメトリクスが取れていなかった。
![1710995491024.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/541daee7-cc59-8f5b-ca3d-f44b71e60aa2.png)

おそらくvSphere with TanzuのPSAが原因と思われるので、一旦ここでは放置する。

次にGrafanaにもアクセスしてみる。
初期アカウントはadmin/prom-operatorでログイン出来るので、こちらでログインする。
![1710995621059.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/d7b03163-814e-895a-7385-ab7b0901c2b8.png)


こちらも問題なくアクセスでき、ダッシュボードも利用することが出来た。
一部のメトリクスが取れておらずNo dataになっているのは、上述のPSAが原因だと思われるのでPrometheus Operator自体はおそらく問題ないと思われる。