これは Kubernetes3 Advent Calendar の 8日目の記事です。

はじめに

k8sのモニタリングの手段はいくつかあると思いますが、Prometheusにもk8sサービスディスカバリ機能があるのでk8sのモニタリングを行う事が出来ます。

Prometheusをk8sクラスタ内にデプロイしてモニタリングするには、Prometheusの設定をk8sのConfigMapに書き、Volume経由でPrometheusのPodに渡して使うと良いでしょう。



k8sクラスタの外にあるPrometheusからk8sクラスタ内のPodのメトリクスを収集したい

ここからがこの記事の本題です。

動機

僕は普段Prometheusを使って自分たちのサービスをモニタリングしていますが、Prometheusサーバは専任のエンジニアが運用をやってくれています。
僕たちのサービスで収集しているメトリクスはかなりの量で、k8sクラスタ毎にPrometheusサーバを配置すると運用が大変になるので避けたいと思っています。

  • サービス開発側でPrometheusサーバの運用する事は避けたい
    (監視対象のサーバやメトリクスが大量になるサービスの場合、Prometheusサーバの運用は大変です)
  • k8sクラスタ毎にPrometheusサーバを配置してPrometheus担当エンジニアに運用してもらう事も避けたい
    (運用コストの面から、Prometheusサーバ の台数を増やしたくない)

以下のようなイメージです。

Prometheusをクラスタ内で動かす場合

こうしたい


考えられる選択肢

以上の理由から、k8sクラスタの中にPrometheusは配置せずに、クラスタ外のPrometheusサーバからモニタリング出来ないかを考えてみました。

パッと思いつくのは、以下の選択肢です。

  • PushProxを使う
    • Prometheus開発者のBrian Brazilさんが開発したPrometheus用のProxyですが、現在は活発に開発はされていないようです
    • 日本語で記事を書かれている方がいらっしゃいます → PrometheusでNATを越えよう!!
    • 現在の開発状況や、上記の紹介記事を読むと、あんまり使いたくない印象を受けました
  • Reverse proxyアプリをクラスタにデプロイして、Service経由で外部のPrometheusサーバからメトリクスを収集する

※なお、「クラスタ内にPrometheusを配置したくない」という今回の目的から外れるので詳しくは書きませんが、クラスタ内にPrometheusを配置しつつクラスタ外のPrometheusと連携させるという方法もあります。

  1. クラスタ内のPrometheusから、クラスタ外のPrometheusに対してRemote writeを行う
  2. Prometheus公式ドキュメント - Storate
  3. Prometheus公式ドキュメント - Remote write tuning
  4. クラスタ外のPrometheusから、クラスタ内のPrometheusに対してRemote readを行う
  5. クラスタ外のPrometheusから、クラスタ内のPrometheusに対してFederationを行う
  6. Prometheus公式ドキュメント - Federation


Reverse proxyアプリを書いてみた

Reverse proxyアプリを書くのは難しくないので自分で書いてみました。

https://github.com/matsumana/psystrike

※Podのメトリクス収集のリクエストをproxyするだけならNginxとかを使っても実現できる気がします。Podのメトリクス収集以外にも実装したい機能が他にもあるので今回は自分でアプリを書きました。


使い方

軽く動作確認してみたところ、うまく動いているようなので使い方を紹介します。

Reverse proxyアプリをクラスタにデプロイする

Docker imageをDocker Hubにアップしてあります。

https://hub.docker.com/r/matsumana/psystrike

Reverse proxyアプリをデプロイするためのManifestファイルは以下のような感じで。
※この例ではServiceのnodePort(31000)でクラスタ外から接続できるようにしています。

例)

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: psystrike
  labels:
    app: psystrike
spec:
  replicas: 1
  selector:
    matchLabels:
      app: psystrike
  template:
    metadata:
      labels:
        app: psystrike
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
        prometheus.io/path: /metrics
    spec:
      containers:
        - name: psystrike
          image: matsumana/psystrike:latest
          ports:
            - containerPort: 8080  # Application
          resources:
            requests:
              memory: "1Gi"
              cpu: "500m"
            limits:
              memory: "1Gi"
              cpu: "500m"
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 60  # Need to wait until the app starts up
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 30  # Need to wait until the app is ready
            periodSeconds: 10
          lifecycle:
            preStop:
              exec:
                command: ["sh", "-c", "sleep 5"]  # Wait a few seconds to service-out from the Service before the SIGTERM signal is sent
      terminationGracePeriodSeconds: 120  # Need to wait until the app shuts down

---
kind: Service
apiVersion: v1
metadata:
  name: psystrike
spec:
  type: NodePort
  selector:
    app: psystrike
  ports:
    - name: app
      protocol: TCP
      targetPort: 8080
      port: 8080
      nodePort: 31000

Prometheusの設定ファイルを変更する

PrometheusからはReverse proxy Podに接続してメトリクスを収集するので、prometheus.ymlのrelabel設定でメトリクスのlabelを変更する必要があります。
そうしないと正しくメトリクスが収集できません。

変更箇所:

  • instance
    • 収集されたメトリクスに付加されるinstanceラベルには、__address__の値が自動的にセットされますが、以下で__address__を書き換えてしまうので明示的に設定する必要があります
  • __metrics_path__
    • /pods/{podのip}/{podのport}/{Podのメトリクスendpoint}`という形式に書き換える
    • ※Reverse proxyアプリ側のリクエストハンドラは以下のようになっています
      @Get("regex:^/pods/(?<host>.*?)/(?<port>.*?)/(?<actualUri>.*)$")
  • __address__
    • デフォルトだと、Podのipとportに対してPrometheusがメトリクス収集に行くのでProxy経由に変更する必要があります

see also:

実際のManifestファイルは以下のようになります。
※Pod用jobの部分だけ抜粋

- job_name: 'kubernetes-pods'
  kubernetes_sd_configs:
    - role: pod
      api_server: https://your_k8s_api_server
      bearer_token: your_k8s_api_server_token
  relabel_configs:
    - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
      action: keep
      regex: true
    - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scheme]
      action: replace
      target_label: __scheme__
      regex: (https?)
    - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port, __meta_kubernetes_pod_annotation_prometheus_io_path]
      action: replace
      target_label: __metrics_path__
      regex: (.+)(?::\d+);(\d+);(.*)
      replacement: /pods/$1/$2$3
    - source_labels: [__address__]
      target_label: instance
    - target_label: __address__
      replacement: your_proxy_app_ip:your_proxy_app_port
    - action: labelmap
      regex: __meta_kubernetes_pod_label_(.+)
    - source_labels: [__meta_kubernetes_namespace]
      action: replace
      target_label: kubernetes_namespace
    - source_labels: [__meta_kubernetes_pod_name]
      action: replace
      target_label: kubernetes_pod_name
    - source_labels: [__meta_kubernetes_pod_node_name]
      action: replace
      target_label: kubernetes_pod_node_name

元のManifestファイルとの差分。

$ diff prometheus_before.yml prometheus_after.yml
14c14
<     - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
---
>     - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port, __meta_kubernetes_pod_annotation_prometheus_io_path]
17,22c17,22
<       regex: (.+)
<     - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
<       action: replace
<       target_label: __address__
<       regex: (.+)(?::\d+);(\d+)
<       replacement: $1:$2
---
>       regex: (.+)(?::\d+);(\d+);(.*)
>       replacement: /pods/$1/$2$3
>     - source_labels: [__address__]
>       target_label: instance
>     - target_label: __address__
>       replacement: your_proxy_app_ip:your_proxy_app_port


Prometheusのk8sディスカバリのしくみ

最後に、ご参考までにPrometheusがどうやってk8sからディスカバリをしているかを説明しておきます。

  1. Prometheusの起動直後、PodやNodeのリストを取得します
  2. その後は変更された差分(追加,変更,削除)だけを取得します
  3. 差分の取得はlong pollingです
  4. long pollingのtimeoutは5分〜10分でランダムに決定されます
# 最初のリスト取得
GET /api/v1/pods?limit=500&resourceVersion=0

# long pollingによる変更差分取得
GET /api/v1/pods?resourceVersion=886756&timeout=8m2s&timeoutSeconds=482&watch=true

long pollingのソースはこの辺りです。
k8sのclientライブラリで実装されていて、Pormetheusはそれを使っています。


概要図


ディスカバリ関連のメトリクス

prometheus_sd_kubernetes_で始まる名前のメトリクスがPrometheusのk8sディスカバリ関連メトリクスです。
メトリクスはいくつかありますが、prometheus_sd_kubernetes_events_totalがPodなどが増減した時に変化するメトリクスです。
long polling以外に、Prometheusが持っているリストを定期的にリフレッシュする処理が動いているようです。



まとめ

  • k8sクラスタの外に配置したPrometheusからクラスタ内のPodのメトリクスを取れるようにReverse proxyを書いてみました
  • 今回は試してませんが、他の選択肢もあります
    • PushProx
    • PrometheusのRemote read/write
    • PrometheusのFederation