🔥

Prometheus, Thanos による k8s クラスタ Metrics

2023/10/21に公開

概要

Observability の 3 本柱として以下の要素があります。

  • Logging
  • Metrics
  • Tracing

前回記事 Filebeat による k8s クラスタ Logging ではこの中の Logging を実装してクラスタのログを収集しました。
今回は 2 つめの Metrics に注目し、k8s クラスタのメトリクスを収集する方法を実装します。

k8s クラスタのメトリクスを収集する際に最もよく使われているのはおそらく prometheus であり、この記事でも prometheus を使用します。

https://prometheus.io/

prometheus をクラスタにインストールする方法はいろいろあります。

  • prometheus pod を手動で作成する。
  • prometheus operator を使ってインストールする。
  • kube-prometheus-stack で構築する。

この中で最も手っ取り早いのは kube-prometheus-stack を使う方法であり、これで構築すると pod レベル、node レベルでのメトリクスを収集するために必要な一連のコンポーネント、メトリクスを可視化するための grafana ダッシュボードのセットアップなどを自動で行ってくれます。

ただ、kube-metrics-stack で構築すると今回の範囲では使用しないコンポーネント (alertmanager など) もまとめてインストールされるため、今回は prometheus operator で必要なリソースを作成してメトリクスを収集する方針で行きます。

prometheus

k8s クラスタのメトリクスとしては、ひとまず以下のメトリクスを収集します。

  • node レベルのメトリクス
  • クラスタのメトリクス

prometheus operator

Prometheus operator ドキュメントの Getting started に沿って operator をインストールします。

https://prometheus-operator.dev/docs/user-guides/getting-started/

LATEST=$(curl -s https://api.github.com/repos/prometheus-operator/prometheus-operator/releases/latest | jq -cr .tag_name)
curl -sL https://github.com/prometheus-operator/prometheus-operator/releases/download/${LATEST}/bundle.yaml | kubectl create -f -

上記の bundle.yml を適用することで、prometheus operator の実行に必要な custom resource definition (CRD) と prometheus operator 本体 (deployment) がインストールされます。

ドキュメントの図にあるように、prometheus operator では ServiceMonitor という CRD を通じで service で公開される metrics を収集する仕組みとなっています。そのため、pod や node のメトリクスを収集するためには metrics を収集するためのエンドポイントとそれに対応する Service, ServiceMonitor オブジェクトを作成する必要があります。
kube-prometheus-stack ではインストール時にこのあたりのオブジェクトが自動で作成されますが、prometheus operator を手動でインストールする場合は ServiceMonitor, Service も手動で作成する必要があります。

以下の作業ではいろいろリソースを作成するため、専用の namespace を作成しておきます。

kubectl create namespace metric

pod メトリクス

pod レベルのメトリクスを収集するには kube-state-metrics を使用します。

https://github.com/kubernetes/kube-state-metrics

helm でインストール。

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install kube-state-metrics prometheus-community/kube-state-metrics

インストールすると kube-state-metrics の pod と service が作成され、[svc_ip_address]:8080/metrics にアクセスすることで prometheus 形式のメトリクスが収集できます。

$ kubectl get svc,pod   -l app.kubernetes.io/name=kube-state-metrics
NAME                                       TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE
service/state-metrics-kube-state-metrics   ClusterIP   10.99.18.80   <none>        8080/TCP   7d8h

NAME                                                    READY   STATUS    RESTARTS       AGE
pod/state-metrics-kube-state-metrics-7c66f4bbcb-p5srx   1/1     Running   1 (6d9h ago)   7d8h

このメトリクス情報を prometheus の収集対象とするため、ServiceMonitor オブジェクトを作成します。作成時に重要なのは以下の点です。

  • spec.selector.matchLabel を 対象の service の labels に設定されている値に一致させる。kube-state-metrics の service では app.kubernetes.io/name: kube-state-metrics が設定されているためそれを指定。
  • spec.endpoints を対象サービスの metrics が公開されるエンドポイントに一致させる。今回の場合、state-metrics の service では以下のように http という name が追加されているので、上記の指定方法で metrics エンドポイントに到達できる。
state-metrics-kube-state-metrics
spec:
  ...
  ports:
  - name: http
    port: 8080
    protocol: TCP
    targetPort: 8080

また、Prometheus インスタンスが ServiceMonitor を収集対象に含めるよう、prometheus インスタンスと一致する値を servicemonitor の labels に設定します。ラベルの値に特に制限はないので、ここでは type: metrics というラベルを設定し、あとでまとめて収集できるようにします。
最終的な serviceMonitor マニフェストは以下のようになります。

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: metrics-kube-state-metrics
  namespace: metrics
  labels:
    type: metrics
spec:
  endpoints:
  - honorLabels: true
    port: http
    path: /metrics
  jobLabel: app.kubernetes.io/name
  selector:
    matchLabels:
      app.kubernetes.io/name: kube-state-metrics

node メトリクス

node のメトリクス収集には prometheus-node-exporter を使います。

https://github.com/prometheus-community/helm-charts/tree/main/charts/prometheus-node-exporter

こちらも helm でインストール。

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install node-exporter prometheus-community/prometheus-node-exporter

これにより daemonset がインストールされ、クラスタの各ノード上に node exporter pod と service が作成されます。
[pod_ip_adress]:9100/metrics で node メトリクスが公開されるため、pod ログと同様に ServiceMonitor を作成します。

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: metrics-node-exporter
  namespace: metrics
  labels:
    app.kubernetes.io/component: metrics
    type: metrics
spec:
  attachMetadata:
    node: false
  endpoints:
  - port: http-metrics
    scheme: http
  jobLabel: jobLabel
  selector:
    matchLabels:
      app.kubernetes.io/component: metrics
      app.kubernetes.io/name: prometheus-node-exporter

prometheus インスタンスの作成

prometheus operator をインストールすると type: Prometheus という CRD が作成されます。この prometheus インスタンスという custom resource を作ることで prometheus pod が起動します。

まず、https://prometheus-operator.dev/docs/user-guides/getting-started/#deploying-prometheus にしたがって prometheus でメトリクスを収集する際に使用される clusterrole, clusterrolebinding, serviceaccount を作成します。

次に prometheus インスタンス用のマニフェストを用意します。

prometheus.yml
apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
  name: prometheus
  namespace: metrics
spec:
  serviceAccountName: prometheus
  serviceMonitorSelector:
    matchLabels:
      type: metrics
  resources:
    requests:
      memory: 400Mi
  enableAdminAPI: true

マニフェスト内の serviceMonitorSelector.matchLabels で、prometheus がメトリクス収集対象とする ServiceMonitor に付加されているラベルを指定します。
先ほど作成した kube-state-metrics, node-exporter 用の ServiceMonitor にはいずれも type: metrics のラベルを付けていたので、これを指定することでメトリクスを収集できます。

マニフェストをデプロイすると、prometheus-prometheus-0 という pod が起動し、この中で prometheus コンテナが稼働します。

$ kubectl get pod
NAME                                                READY   STATUS    RESTARTS      AGE
node-exporter-prometheus-node-exporter-r5wqm        1/1     Running   1 (11d ago)   12d
node-exporter-prometheus-node-exporter-xvw44        1/1     Running   1 (11d ago)   12d
prometheus-prometheus-0                             3/3     Running   0             7d3h
state-metrics-kube-state-metrics-7c66f4bbcb-p5srx   1/1     Running   1 (11d ago)   12d

Thanos

あとは prometheus の web UI や Grafana を使ってメトリクスを可視化することができますが、 prometheus operator 単体の構成では実際の運用を考えた際に以下のような課題点があります。

  • メトリクスデータを一時的にしか保存できないため、永続的のためには別途ストレージを用意する必要がある。
  • 複数の k8s クラスタを管理する場合のデータ管理・可視化がやや面倒。

このような課題に対応するため、 OSS の Thanos を組み合わせます。

https://thanos.io/

Thanos の特徴はドキュメントにいろいろ書かれていますが、主に以下のメリットがあります。

  • prometheus が収集したデータを外部オブジェクトストレージに保存し永続化できる。
  • 複数のクラスタに存在する prometheus にまとめてクエリを実行できる (Global Query)。
  • クラスタ毎のメトリクスにラベルが追加でき、メトリクスをクラスタ毎に比較したり合計値を取るなどの演算ができる。
  • prometheus 互換であり、クエリ API そのままでメトリクスを取得できる。

ドキュメントの Design にも書いてあるように、単にクラスタ 1 つで prometheus も 1 つという状況ではそれほどメリットはありませんが、管理するクラスタが複数の場合や大規模な環境で Prometheus の availability を考えて運用していく場合に効果を発揮するプロジェクトとなっています。CNCF の Incubating project に位置づけられていることもあり、k8s やクラウド環境で prometheus を運用する上でよく使われているようです。

Thanos is a set of components that can be composed into a highly available Prometheus setup with long term storage capabilities. Its main goals are operation simplicity and retaining of Prometheus’s reliability properties.
The Prometheus metric data model and the 2.0 storage format (spec, slides) are the foundational layers of all components in the system.

Thanos はいくつかのコンポーネントから構成されているため、以降では既に導入した prometheus 環境に各コンポーネントを導入する手順について見ていきます。

Sidecar

thanos sidecar は prometheus と接続し、以下の役割を持ちます。

  • prometheus が収集したメトリクスをオブジェクトストレージにアップロードする。
  • querier による store API (後述) を受けてクエリに対応する。

sidecar は prometheus operator に組み込まれているため、prometheus インスタンスのマニフェスト定義を書き換えることで pod 内に sidecar コンテナとして注入できます。

prometheus.yml
spec:
  ...
+  thanos:
+    image: quay.io/thanos/thanos:v0.32.4

prometheus インスタンスを再度デプロイして kubectl describe statefulsets を実行すると、thanos-sidecar コンテナが pod 内に追加されていることが確認できます。

   thanos-sidecar:
    Image:       quay.io/thanos/thanos:v0.32.4
    Ports:       10902/TCP, 10901/TCP
    Host Ports:  0/TCP, 0/TCP
    Args:
      sidecar
      --prometheus.url=http://localhost:9090/
      --prometheus.http-client={"tls_config": {"insecure_skip_verify":true}}
      --grpc-address=:10901
      --http-address=:10902
      --tsdb.path=/prometheus
    Mounts:
      /prometheus from prometheus-prometheus-db (rw)

デフォルトの設定ではオブジェクトストレージのアップロードが行われず、同じ pod に存在する prometheus コンテナが収集したメトリクスを query API で取得するように対応するような動作となります。

Querier

thanos querier は store API と呼ばれるクエリを実行し、sidecar やオブジェクトストレージからメトリクスを取得するコンポーネントとなっています。単にメトリクスを可視化するのであれば prometheus に直接クエリを投げればいいですが、querier を使用することで複数箇所に存在する thanos sidecar に対してまとめてクエリを実行したり、オブジェクトストレージに保存した過去のメトリクスデータを参照できる等の特徴があります。

prometheus operator と同じクラスタにインストールすることも可能ですが、メトリクスはどこか 1 つのクラスタで中央集権的に収集・可視化する構成としたいので、ここでは prometheus を構築したクラスタとは別のクラスタを用意し、そこにインストールします。


別クラスタにインストール場合、 thanos query -> sidecar の store API が通るようにする。

クラスタを分ける際は querier -> sidecar に対する store API (gRPC) が通信できる経路をつくる必要があります。
まずクラスタ外部から thanos sidecar に通信できるよう service, ingress を作成します。

service.yml
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/name: thanos-sidecar
  name: thanos-sidecar
  namespace: metrics
spec:
  type: ClusterIP
  ports:
  - name: grpc
    port: 10901
    targetPort: grpc
  - name: http
    port: 10902
    targetPort: http
  selector:
    prometheus: prometheus
    app.kubernetes.io/instance: prometheus

Ingress について、nginx ingress controller を使用している場合、annotations: nginx.ingress.kubernetes.io/backend-protocol: "GRPC" を指定して grpc をルーティングできるようにします。詳しくはドキュメント を参照。

ingress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: prometheus
  namespace: metrics
  annotations:
    nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
spec:
  ingressClassName: nginx
  rules:
    - host: thanos-sidecar.ops.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: thanos-sidecar
                port:
                  number: 10901

これでクラスタ外部の querier から sidecar にアクセスできるようになったので、querier をインストールするマニフェストを作成します。
querier では実行時引数の --endpoint で接続する sidecar の [ip address]:[grpc port] を指定します。今回の環境では nginx ingress controller の https を nodePort 32324 で設定しているので、--endpoint=thanos-sidecar.ops.com:32324 を指定します。
最終的なマニフェストは以下の通り。

thanos-querier.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/name: thanos-query
  name: thanos-query
  namespace: metrics
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: thanos-query
  template:
    metadata:
      labels:
        app.kubernetes.io/name: thanos-query
    spec:
      containers:
      - args:
        - query
        - --http-address=0.0.0.0:19192
        - --query.replica-label=prometheus_replica
        - --query.replica-label=thanos_ruler_replica
        - --endpoint=thanos-sidecar.ops.com:32324
        - --grpc-client-tls-secure
        - --grpc-client-tls-skip-verify
        image: quay.io/thanos/thanos:v0.32.4
        name: thanos-query
        ports:
        - containerPort: 10901
          name: grpc
        - containerPort: 19192
          name: web

また、thanos querier は prometheus 互換の web UI を備えており、上記で http-address に指定した port (19192) を開くと prometheus と同じ UI にアクセスできます。
UI では prometheus 同様にクエリを実行してメトリクスを可視化したり、接続に成功している endpoint が確認できます。


querier の UI。prometheus と同様の見た目でクエリ等でメトリクスを可視化できる。

オブジェクトストレージへの保存・クエリ

上記ではただ sidecar を配置するだけでしたが、設定ファイルを用意することで prometheus が収集したメトリクスを定期的に外部オブジェクトストレージに送信できます。対応しているオブジェクトストレージはドキュメントに記載されています。

主要なクラウドのオブジェクトストレージサービスを使うのが手っ取り早いですが、今回は無料でやりたいので OSS の S3 互換オブジェクトストレージ minio をローカルに立てて動作を確認します。

https://min.io/

minio はクラスタとは別のノードに docker-compose で構築。

docker-compose.yml
services:
  minio:
    image: quay.io/minio/minio:latest
    container_name: minio
    restart: always
    ports:
      - 9000:9000
      - 9090:9090
    volumes:
      - ~/minio/data:/data
    command:
      - "server"
      - "/data"
      - "--console-address"
      - ":9090"
    environment:
      - "MINIO_ROOT_USER=admin"
      - "MINIO_ROOT_PASSWORD=[password]

構築後はメトリクスを保存するための bucket test1 を作成しておきます。

次に、thanos sidecar が minio 上の bucket に接続するための認証情報を作成します。minio は S3 互換であるため、ドキュメントの type: S3 の記述形式 に沿ってファイルを作成します。
設定項目は色々ありますが、http で接続するためには最低限以下が設定されていれば良いです。

minio.yml
type: S3
config:
  bucket: test1
  endpoint: minio.centre.com:9000
  access_key: admin
  insecure: true
  signature_version2: false
  secret_key: [password]
prefix: ""

これを secret として作成。

$ kubectl create secret generic minio-secret --from-file=minio.yml --dry-run=client -o yaml > minio-secret.yml
$ kubectl apply -f minio-secret.yml

sidecar が オブジェクトストレージにメトリクスをアップロードするには、sidecar の引数 --objstore.config-file--objstore.config で指定する必要があります。
prometheus operator 経由でインストールする場合は prometheus のマニフェストで thanos.objectStorageConfig に secret 名と key を指定すれば自動で設定してくれます。

prometheus.yml
piVersion: monitoring.coreos.com/v1
kind: Prometheus
...
spec:
  ...
  thanos:
    image: quay.io/thanos/thanos:v0.32.4
    objectStorageConfig:
      key: minio.yml
      name: minio-secret

prometheus インスタンスを再度デプロイすることで thanos sidecar がメトリクスを定期的に minio にアップロードするようになります。デフォルト設定ではアップロード間隔は 2 時間毎となっています ( prometheus がデータを block に分割する期間に対応)。
しばらく待ってから minio UI で対象の bucket にアクセスすると、アップロードされたデータが格納されている様子が確認できます。

各ディレクトリの中身は chunk, index, metadata となっており sidecar ドキュメント に説明があります。

オブジェクトストレージに保存したメトリクスは thanos store コンポーネントを用意することで参照できるようになります。
store も prometheus operator には組み込まれていないため、querier と同様に deployment のマニフェストを作成します。
store もオブジェクトストレージに接続するための認証情報が必要となります。sidecar と同様のファイルが使用できるため、secret を pod 内に mount した上で objstore.config-file に設定します。

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/name: thanos-store
  name: thanos-store
  namespace: metrics
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: thanos-store
  template:
    metadata:
      labels:
        app.kubernetes.io/name: thanos-store
    spec:
      containers:
        - args:
            - store
            - --data-dir=/data
            - --grpc-address=0.0.0.0:10901
            - --http-address=0.0.0.0:10902
            - --objstore.config-file=/bucket-config/bucket.yml
          image:
          name: thanos-store
          ports:
          - containerPort: 10902
            name: http
          - containerPort: 10901
            name: grpc
          volumeMounts:
            - name: bucket-config
              mountPath: /bucket-config
      volumes:
        - name: bucket-config
          secret:
            secretName: bucket-secret

また、querier -> store の通信のため service を作成します。

apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/name: thanos-store
  name: thanos-store
  namespace: metrics
spec:
  type: ClusterIP
  ports:
  - name: grpc
    port: 10901
    targetPort: grpc
  - name: http
    port: 10902
    targetPort: http
  selector:
    app.kubernetes.io/name: thanos-store

sidecar に加えて store をクエリ対象に含めるため、querier のマニフェストに endpoint を追加します。

thanos-querier.yml
    spec:
      containers:
      - args:
        - query
        - --http-address=0.0.0.0:19192
        - --query.replica-label=prometheus_replica
        - --query.replica-label=thanos_ruler_replica
        - --endpoint=thanos-sidecar.ops.com:32324
+        - --endpoint=thanos-thanos-store:10901

上記のマニフェストを適用することで store pod が作成され、querier -> store でオブジェクトストレージ上のメトリクスが取得できます。
接続が正常に成功していることは querier の pod log や querier UI で store の endpoint が追加されていることから確認できます。

prometheus のデータを永続化していない場合、収集したメトリクスは一定期間を経過すると削除されますが、このように thanos と組み合わせることで過去のメトリクスをオブジェクトストレージに保存し、必要な時に参照できるようになります。

複数クラスタからのメトリクス収集

ここまでは単一クラスタにインストールした prometheus でメトリクスを収集していましたが、
収集したいクラスタが増えた際も同様の手順で対応できます。
thanos query, store は上記の手順で既に管理用クラスタにインストールされているため、新しいクラスタでは以下のコンポーネントをインストールすればメトリクスが収集できるようになります。

  • prometheus operator
  • kube-state-metrics
  • node-exporter


メトリクス対象クラスタが 2 つになった場合の概要図

Label の設定

thanos を使うと querier で複数の prometheus メトリクスをまとめて収集できますが、それぞれのメトリクスを区別する情報が必要になります。
thanos では ドキュメント にも記載されているように、それぞれの prometheus インスタンスが収集したメトリクスを一意に識別できるような external_labels を設定することを推奨しています。

今回はメトリクスがどのクラスタから収集されたものか区別ができれば良いので、クラスタ名をラベルに設定します。prometheus operator では spec.externalLabels に key-value のラベルを設定できるので、マニフェスト内で指定します。

prometheus.yml
spec:
  externalLabels:
    cluster: "k8s-dev"

もう一つのクラスタにインストールするマニフェストには別のクラスタ名を設定します。

prometheus.yml
spec:
  externalLabels:
    cluster: "k8s-dev2"

これらのマニフェストを各クラスタ上で再インストールすると、external_labels に設定した key-value の値が各メトリクスにラベルとして追加されます。


メトリクスにcluster ラベルが追加されている様子

もちろん instance や pod id のようなデフォルトで設定されるラベルからメトリクスを区別することも可能ですが、意味のある値をラベルに設定することでメトリクスの可読性や集計の汎用性が高まります。

その他、Grafana でメトリクスを可視化することで複数クラスタのメトリクスをまとめて比較したり、Grafana の Variables を使うことでドロップダウンで表示するクラスタを選択するといった表現が可能です。



ドロップダウンでメトリクスを表示するクラスタを切り替えられる

おわりに

Prometheus と Thanos を組み合わせることで複数クラスタのメトリクスを収集・可視化することができます。特に thanos はスケール性に優れており、収集対象の複数クラスタが存在する場合にどこか 1 箇所で集権的に収集、grafana でまとめて可視化・分析といった用途に有用です。

Discussion