オブジェクトの広場はオージス総研グループのエンジニアによる技術発表サイトです

コンテナ・マイクロサービス

Kubernetes活用への道のり

第5回 ロギング・モニタリング編 Prometheus・Grafanaで可視化してみよう
オージス総研 技術部 アドバンストテクノロジセンター
山中 克容
2022年3月17日

本連載ではKubernetesやマイクロサービスを活用するにあたりどんな準備を進めておけばいいか整理します。第5回はPrometheus・Grafanaを活用してモニタリング、ロギングする方法を紹介します。

1. はじめに

前回Kubernetes上にDaprを活用したマイクロサービスアプリケーションの導入まで実施しました。アプリケーションが稼働するところまでは出来ましたので、今回はKubernetesやその上で動作するアプリケーションのモニタリングやログの可視化を行います。ここまでで本連載としては一区切りになります。

また、今回は検証パターンとして、Kubernetes内ですべてリソースを用意するケースと、一部AWSのマネージドサービスを利用する例を紹介します。

2. モニタリング

モニタリングではリソース(CPU、メモリ、ディスクなど)の使用量やパフォーマンス、アプリケーションの死活監視などを行います。モニタリングにはPrometheusを使用しますが、Prometheus本体だけではモニタリングに必要な機能を満たせませんので、周辺ツールも含むprometheus-operator※を利用して導入します。

※ Operatorとはカスタムリソースを定義することでKubernetesの機能を拡張し、複雑なアプリケーションの導入や運用などを自動化する仕組みになります。

2.1 アーキテクチャ

prometheus-operatorを利用して環境を構築した場合、下記のようなアーキテクチャになります。ServiceMonitorリソースでターゲットとなるServiceを指定し、PrometheusはServiceMonitorで定義されているサービスからメトリクスを収集します。(Serviceを定義していないPod用にPodMonitorもあります)

prometheus

メトリクスの収集はPrometheus側からPullする仕組みのため、矢印はPrometheusから収集先に向かっています

ノードに関するメトリクスはnode-exporterから、コンテナに関するメトリクスはcAdvisorから収集します。prometheus-operatorを導入するとこれらのメトリクスを収集するためのServiceMonitorが標準で導入されます。アプリケーションが出力するメトリクスの監視は個別にServiceMonitorを導入して監視します。

2.2 Prometheusの導入

それではprometheus-operatorを使用してPrometheusを導入します。

導入コンポーネントの一覧は公式サイトにある通り以下のものになります。

  • The Prometheus Operator
  • Highly available Prometheus
  • Highly available Alertmanager
  • Prometheus node-exporter
  • Prometheus Adapter for Kubernetes Metrics APIs
  • kube-state-metrics
  • Grafana

また、上記には直接記載はありませんが、導入するPrometheusやGrafana等を監視するためのServiceMonitorやAlertRuleなども導入されます。

今回は第1回で構築したEKSおよびArgoCDの環境への導入を前提としますので、ArgoCDのアプリケーションリソースを用意して導入します。(Kubernetesのバージョンは1.21とします)

(1) kube-prometheusをclone

prometheus-operatorにはオペレータ本体の開発に使われているリポジトリ(prometheus-operator)と、開発したprometheus-operatorを導入するリポジトリ(kube-prometheus )があります。そのため、導入の際はkube-prometheusリポジトリをcloneします。kube-promehtuesはバージョンごとにサポートする(テスト済み)Kubernetesのバージョンが異なります。 サポートするバージョンのマトリクス表が公式のドキュメントにありますので参考にしてください。

今回は、kubernetes1.21に導入するため、release-0.9をcloneします。

git clone https://github.com/prometheus-operator/kube-prometheus.git --branch release-0.9

cloneしてきたら、manifestsディレクトリ配下をArgoCDから参照できるように自身のリポジトリに配置します。

ディレクトリ構成

リポジトリルート
└manifests
 └setup 

setupディレクトリにはprometheus-operatorを使用するためのカスタムリソースが定義されています。manifests直下にはprometheus-operatorを導入するためのマニフェストが定義されています。

(2) kube-prometheusの導入

ArgoCDのアプリケーションとしてkube-prometheusを導入します。

注意点として、2022年3月現在prometheus-operatorはkubectl applyコマンドでのカスタムリソース導入でエラー(カスタムリソースのサイズが大きすぎるため)が発生します。ArgoCDでデプロイする際も通常内部ではkubectl applyでのデプロイを行うため同様にエラーが発生します。 ArgoCDの公式にapplyが失敗するときの対応手順として、Replace=trueをつけることでkubectl replace or kubectl createでデプロイされると記載がありますので、その方式を利用してデプロイします。

カスタムリソースの導入(application_crd.yaml)

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: prometheus-operator-crd
  namespace: argocd
spec:
  destination:
    namespace: observation
    server: https://kubernetes.default.svc
  project: default
  source:
    path: 'manifests/setup'
    repoURL: https://github.com/<ユーザー>/<リポジトリ>
    targetRevision: HEAD
  syncPolicy:
    automated:
      prune: true
    syncOptions:
      - CreateNamespace=true
      - Replace=true

kube-prometheus本体の導入(application.yaml)

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: prometheus-operator
  namespace: argocd
spec:
  destination:
    namespace: observation
    server: https://kubernetes.default.svc
  project: default
  source:
    path: 'manifests/'
    repoURL: https://github.com/<ユーザー>/<リポジトリ>
    targetRevision: HEAD
  syncPolicy:
    automated:
      prune: true
    syncOptions:
      - CreateNamespace=true

2.3 Prometheus、Grafanaの動作確認

リソースの作成が完了したら、ブラウザで起動していることを確認します。

(1) 導入サービスの一覧を確認

kubectl get svc -n monitoring

NAME                    TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
alertmanager-main       ClusterIP   10.100.61.139    <none>        9093/TCP,8080/TCP            7d22h
alertmanager-operated   ClusterIP   None             <none>        9093/TCP,9094/TCP,9094/UDP   7d22h
blackbox-exporter       ClusterIP   10.100.254.165   <none>        9115/TCP,19115/TCP           7d22h
grafana                 ClusterIP   10.100.142.192   <none>        3000/TCP                     7d22h
kube-state-metrics      ClusterIP   None             <none>        8443/TCP,9443/TCP            7d22h
node-exporter           ClusterIP   None             <none>        9100/TCP                     7d22h
prometheus-adapter      ClusterIP   10.100.124.16    <none>        443/TCP                      7d22h
prometheus-k8s          ClusterIP   10.100.23.165    <none>        9090/TCP,8080/TCP            7d22h
prometheus-operated     ClusterIP   None             <none>        9090/TCP                     7d22h
prometheus-operator     ClusterIP   None             <none>        8443/TCP                     7d22h

(2) ポートフォワードしてローカルのブラウザで確認

Prometheus

kubectl port-forward svc/prometheus-k8s -n monitoring 9090:9090

動作確認URL

http://localhost:9090

Grafana

kubectl port-forward svc/grafana -n monitoring 3000:3000

動作確認URL

http://localhost:3000

AlertManager

kubectl port-forward svc/alertmanager-main -n monitoring 9093:9093

動作確認URL

http://localhost:9093

Grafanaのみログインが必要になるので、ID:admin PW:adminで初回ログインします。

3. モニタリング設定

導入時点でノードやコンテナのモニタリングはできるようになっているので、実際にPrometheusから確認してみます。Prometheusの画面にアクセスしてGraphタブから確認します。(Grafanaで確認することも可能です)

なお、Prometheusでのデータ取得にはPromQLを使用します。クエリの文法等はPrometheusのサイトを参照してください。

3.1 ノードのモニタリング

各ノードでnode-exporterのDaemonSetが起動しているので、Prometheusの画面でノードの状態が確認できます。

例)CPU(user)の使用率

rate(node_cpu_seconds_total{mode="user"}[1m])

Prometheus

3.2 コンテナのモニタリング

コンテナのモニタリングもkubeletのServiceでメトリクスが公開されているため、確認することができます。

例)PodごとのCPU使用時間

sum(rate(container_cpu_usage_seconds_total[5m])) by (pod)

Prometheus

3.3 アプリケーションのモニタリング

次にアプリケーションが出力したメトリクスをPrometheusで可視化してみます。サンプルとしてリクエスト数をmetricsとして出力するアプリケーションを用意して、その値をPrometheusで確認してみます。

(1) サンプルアプリケーションの準備

サンプルのGoアプリケーションを用意します。サンプルアプリケーションで実装する内容は下記になります。

リクエストのエンドポイント

サンプルでは以下2つのエンドポイントを用意します。

  • “/”
    • リクエストを受信するとPrometheusのCounterコレクタを使用して受信回数をインクリメントします。このエンドポイントはテストのために実際にブラウザなどからアクセスするものになります。
  • “/metrics”
    • “/"エンドポイントアクセス時にインクリメントしているカウンタ値をメトリクスとして応答します。このエンドポイントはPrometheusから参照するものになります。

コードの抜粋

import (
  ...
  "github.com/prometheus/client_golang/prometheus"
  "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
  httpReqs = prometheus.NewCounterVec(
    prometheus.CounterOpts{
      Name: "http_request_total",
      Help: "Number of HTTP Request.",
    },
    []string{"path"},
  )
)

func init() {
  prometheus.MustRegister(httpReqs)
}

func metrics(w http.ResponseWriter, r *http.Request) {
  promhttp.Handler().ServeHTTP(w, r)
}

func handler(w http.ResponseWriter, r *http.Request) {
  m := httpReqs.WithLabelValues("/")
  m.Inc()
  fmt.Fprint(w, string("hello world"))
}

func main() {
  http.HandleFunc("/metrics", metrics)
  http.HandleFunc("/", handler)
  http.ListenAndServe(":18080", nil)
}

作成したアプリケーションは「app: demo-service」ラベルを持つアプリケーションとしてEKSへデプロイします。

(2) ServiceMonitor

アプリケーションのServiceをターゲットとするServiceMonitorリソースを作成します。この例では、defaultネームスペースにある「app: demo-service」ラベルを持つサービスをターゲットに「spec.ports.name: http」のポートからメトリクスを収集するServiceMonitorが作成されます。

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: servicemonitor-demo-service
  namespace: monitoring
  labels:
    serviceapp: demo-service
    release: prometheus-operator
spec:
  selector:
    matchLabels:
      app: demo-service
  endpoints:
  - port: http
    interval: 30s
  namespaceSelector:
    matchNames:
    - default

(3) Prometheus(Grafana)で確認

サンプルアプリケーションに何度かアクセスした後、5分間のリクエストレートを出力してみます。

rate(http_request_total{}[5m])

Prometheus

4. ロギング

次はアプリケーションログを収集します。今回はGrafanaLokiとPromtailを利用した構成で構築します。

4.1 アーキテクチャ

PromtailとGrafanaLokiを利用したログ収集の仕組みは下記のようになります。各ノードにDaemonsetとしてデプロイしたPromtailが標準出力ログを収集し、GrafanaLokiでログを永続化します。Grafanaでは永続化したログを可視化します。

Prometheus

4.2 GrafanaLoki・Promtailの導入

導入はこちらもArgoCDのアプリケーションとして導入していきます。

(1) GrafanaLoki

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: loki
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  destination:
    namespace: monitoring
    server: 'https://kubernetes.default.svc'
  source:
    chart: 'loki'
    repoURL: 'https://grafana.github.io/helm-charts'
    targetRevision: 2.9.1
    helm:
      version: v3
      releaseName: loki
      values: |
        replicas: 1
        persistence:
          enabled: true
  project: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: false
    syncOptions:
      - CreateNamespace=true

(2) Promtail

Promtailは導入時にHelmのValuesに収集条件を設定します。下記例では「app: demo-***」ラベルを持つアプリケーションを対象に、JSON形式のログを収集しています。その際level、id、url、methodでラベル付けをしてGrafanaで検索できるようにしています。また、metrics条件ではエラー件数をPrometheusで検出できるように設定しています。(ここについては後程利用します)

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: promtail
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  destination:
    namespace: monitoring
    server: 'https://kubernetes.default.svc'
  source:
    chart: 'promtail'
    repoURL: 'https://grafana.github.io/helm-charts'
    targetRevision: 3.11.0
    helm:
      version: v3
      releaseName: promtail
      values: |
        serviceMonitor:
          enabled: true
        config:
          lokiAddress: http://loki:3100/loki/api/v1/push
          snippets:
            pipelineStages:
            - docker:
            - match:
                selector: '{app=~"demo-.*"}'
                stages:
                - json:
                    expressions:
                      level: level
                      id: id
                      url: url
                      method: method
                - labels:
                    level:
                    dt:
                    url:
                    method:
                - metrics:
                    log_error_total:
                      type: Counter
                      description: error number
                      prefix: promtail_custom_
                      source: level
                      config:
                        value: ERROR
                        action: inc
  project: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: false
    syncOptions:
      - CreateNamespace=true

4.3 Grafanaでログを確認

(1) Grafanaの設定

GrafanaでLokiが格納したログデータを参照できるように設定します。

Lokiのサービスを確認

kubectl get svc -n monitoring

NAME                    TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
...
loki                    ClusterIP   10.100.108.199   <none>        3100/TCP                     6m18s
loki-headless           ClusterIP   None             <none>        3100/TCP                     6m18s
...

Lokiはサービス名loki、ポート3100で待ち受けしているのがわかりますので、GrafanaのURLに指定します。

Grafana

(2) アプリケーションでログを出力

3.3で用意したアプリケーションにログ出力を追加します。

ログ出力処理の追加部分を抜粋

type logJson struct {
    Level   string `json:"level"`
    Dt      string `json:"dt"`
    Id      string `json:"id"`
    Url     string `json:"url"`
    Method  string `json:"method"`
    Message string `json:"message"`
}

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Println(logStr(&logJson{Level: "INFO", Id: "1234-56789", Dt: fmt.Sprint(time.Now()), Url: "http://localhost:18080/", Method: "handler", Message: "start handler"}))
  m := httpReqs.WithLabelValues("200", "GET")
  m.Inc()
  fmt.Fprint(w, string("hello world"))
    fmt.Println(logStr(&logJson{Level: "INFO", Id: "1234-56789", Dt: fmt.Sprint(time.Now()), Url: "http://localhost:18080/", Method: "handler", Message: "start handler"}))
}

(3) Grafanaで確認

4.2のPromtail導入時にlevel、dt、url、methodのラベルを定義しているのでラベル指定でログ出力してみます。

例)levelがINFOとなっているログを表示

{level="INFO"}

Grafana

5. アラート

最後にモニタリング結果やログの内容からアラートを生成します。CPUを使いすぎたり、エラーログを出力した際に検知することが目的になります。

5.1 アーキテクチャ

Prometheusで収集したメトリクスをアラートルールに基づいてAlertManagerへ連携します。AlertManagerから各ツール(Slack、メール等)に連携する仕組みになります。AlertManagerはアラートの一覧確認や一時的な抑制などを行うためのツールになります。

Grafana

5.2 アラート対象のモニタリング

アラート対象として、エラーログが出力された際にSlackへ通知する例を作成します。エラーログのモニタリング例は4.2のPromtail導入時に設定したHelmの下記設定値部分になります。 この記述ではlevel=ERRORのログが出力された際にカウントを取り、それをPrometheusへメトリクスとして出力しています。(メトリクスはprefixとメトリクス名をつなげた名前で出力されるため、この例ではpromtail_custom_log_error_totalとなります)

                - metrics:
                    log_error_total:
                      type: Counter
                      description: error number
                      prefix: promtail_custom_
                      source: level
                      config:
                        value: ERROR
                        action: inc

次に上記メトリクス結果をアラートとして認識するようにアラートルールを設定します。この例では1分間にエラーが1つ以上出力されたら「severity: critical」ラベルを付与したアラートを作成します。

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: demo-alert-rules
  namespace: monitoring
  labels:
    app: kube-prometheus-stack
    release: prometheus-operator
spec:
  groups:
- name: rules-demo-alert
    rules:
    - alert: ApplicationError
      expr: >-
          count (promtail_custom_log_error_total) > 0
      for: 1m
      labels:
        severity: critical
      annotations:
        message: >-
          {{ $labels.job }}/{{ $labels.service }} targets in {{ $labels.namespace }} namespace are application error.

5.3 AlertManager設定

AlertMangerでは主にreceivers句とroute句を設定します。receivers句ではアラートの送信先に関する設定を定義し、route句ではどの条件の場合にどのレシーバに流すかを定義します。

今回の例ではroute句で「severity: critical」ラベルのついた通知をslack_notificationsレシーバへ送信するように設定しているため、「severity: critical」ラベルのついたアラートはSlackへ通知されます。なお、Slackへの通知はSlack側にIncoming Webhookを導入しておく必要があります。

apiVersion: v1
kind: Secret
metadata:
  labels:
    alertmanager: main
    app.kubernetes.io/component: alert-router
    app.kubernetes.io/name: alertmanager
    app.kubernetes.io/part-of: kube-prometheus
    app.kubernetes.io/version: 0.22.2
  name: alertmanager-main
  namespace: monitoring
stringData:
  alertmanager.yaml: |-
    "global":
      "resolve_timeout": "5m"
      "slack_api_url": "<slack_webhook_url>"
    "receivers":
    - "name": "Default"
    - "name": "slack_notifications"
      "slack_configs":
      - "channel": "#<チャンネル名>"
        "send_resolved": true
    "route":
      "group_by":
      - "namespace"
      "group_interval": "5m"
      "group_wait": "30s"
      "receiver": "Default"
      "repeat_interval": "12h"
      "routes":
      - "match":
          "severity": "critical"
        "receiver": "slack_notifications"
type: Opaque

これで、エラーログが出力されるとSlackへ通知が行われるようになります。

6. AWSマネージドサービスの利用

ここまでKubernetes内に必要なリソースをすべて作成してきましたが、一部マネージドサービスを利用してみたいと思います。永続データなどステートフルな情報をKubernetesではなくマネージドサービスで管理したい場合に利用できるアーキテクチャになります。

6.1 GrafanaLokiのデータ格納先をS3にする

GrafanaLokiのデータをS3で永続化するように設定を変更します。格納先をS3にすることで高可用性の担保ができるほか、バックアップやデータのライフサイクル等をマネージド管理にすることが可能になります。

それでは、4.2で導入したLokiのアプリケーションファイルを変更して、S3に格納するようにします。設定は公式サイトの例を参考にしています。

        extraArgs:
          target: all,table-manager
        replicas: 1
        persistence:
          enabled: true
        config:
          schema_config:
            configs:
            - from: 2020-05-15
              store: aws
              object_store: s3
              schema: v11
              index:
                prefix: loki_
          storage_config:
            aws:
              s3: s3://ap-northeast-1/<バケット名>
              dynamodb:
                dynamodb_url: dynamodb://ap-northeast-1

EKS上のコンテナからS3やDynamoDBにアクセスする必要がありますので、適切な権限設定が必要になります。必要な権限は公式ドキュメントに記載があります。

なお、こちらのIssueに記載がありますが、ドキュメント通りの設定だけではDynamoDBのテーブルが作られず、公式ドキュメントの設定に加え下記追加の設定をしていますのでご注意ください。

        extraArgs:
          target: all,table-manager

6.2 その他マネージドサービスの利用

今回はGrafanaLokiのデータをS3で永続化しましたが、その他にもAWSではPrometheusやGrafanaのマネージドサービスもあります。これらもコスト見合いにはなるとは思いますが、インフラの可用性等でマネージドサービスの恩恵を受けるメリットがありますので、要件によっては利用してみてはいかがでしょうか。

7. おわりに

Kubernetes関連として、ここまで全5回でIaCによるインフラやCI/CD環境構築、開発用ツールの紹介、分散アプリケーション開発、運用監視と順を追って連載してきました。また、マイクロサービスのテスト技法についても連載していますので、テスト技法について知りたい方はご覧いただければと思います。

連載はこれで一区切りですが、これまで連載を通してKubernetesや周辺のエコシステムを検証していて、便利なところだけではなく、使いにくいと感じたことやこれからの成長に期待したいところなどもありました。次回はこれまでの内容を振り返り、実際に使ってみて良かった点や課題に感じている点などを記事にしてみたいと思います。