📊

【Grafana Loki】Kubernetes環境でのログ管理システムの構築と運用の基本

に公開

はじめに

こんにちわ!SREエンジニアのKoseです!
Grafanaっていいですよね〜。
最近、オブザーバビリティの技術にどハマり中で、ログ管理ツールのGrafana Lokiを調べていました。
そこで、導入時に詰まることがあったので、自分の理解のためにも利用方法をご紹介したいと思います。

前提知識と環境

  • 基本的なLinuxコマンドの知識
  • Kubernetesの基本的な操作
  • ログ管理の基本的な概念
  • Kubernetesクラスタが構築されていること(この記事ではEKSで構築してます。)
  • Prometheus, Grafanaが環境に実装されていること

利用する技術

技術 説明 バージョン
Grafana Loki ログ管理システム 3.4.2
Grafana 可視化ツール 11.6.1
Prometheus メトリック収集 v3.3.1
Fluent Bit ログ収集エージェント v4.0.1
EKS コンテナオーケストレーションプラットフォーム v1.31
Helm Kubernetesパッケージマネージャー v3.17.3

本文

Grafana Lokiについて

Lokiは、Grafana Labsが開発したPrometheusにインスパイアされた水平スケーラブルで可用性の高いマルチテナントのログ集約システムです。Prometheusとは異なり、メトリクスではなくログに焦点を当て、PullではなくPushでログを収集します。

また、Lokiはログのラベルに関するメタデータ(Prometheusのラベルのようなもの)のみをインデックス化するという考えに基づいて構築されています。ログデータ自体は圧縮され、Amazon Simple Storage Service (S3)やGoogle Cloud Storage (GCS)などのオブジェクトストアや、ファイルシステム上にローカルに保存されます。

アーキテクチャ

Lokiのアーキテクチャを理解するために、簡単な構成図を作成してみました。
(🟦:Write、🟥:Readのデータフローと思っていただければ・。・)

Lokiのアーキテクチャ構成図

以下に主要なコンポーネントを簡単にまとめます。
主要なコンポーネントの役割:

  1. Distributor: クライアントから送られてくるログデータ(Pushリクエスト)を最初に受け取る役割を持つ、Lokiの書き込みパスの入り口です
  2. Ingester: 受け取ったログデータを一時的にメモリに保持し、最終的に長期保存先(S3、GCS、Azure Blobなど)に書き込む役割を担います。また、読み取り時には、最近取り込んだインメモリのデータも返します。
  3. Querier: LogQLクエリの実行役です。実際にログデータを検索・取得し、結果を返すコンポーネントです。
  4. Query Frontend: 読み取り処理のアクセラレータです。QuerierのAPIエンドポイントを持ち、読み取りリクエストの最適化や負荷分散を行います。
  5. Ruler: Lokiにおけるルール評価・アラート管理を行うコンポーネントです。Prometheusと同様に、ルールを使ってログを監視し、アラートを発火させます

その他のコンポーネントについては公式ドキュメントに詳しく載っているのでそちらを参照ください

デプロイモード

Lokiは以下の3つのデプロイモードをサポートしています。

  1. Monolithic mode

    • 単一のバイナリとして1つのプロセス内で動作
    • 小規模環境向け
    • 開発・テスト環境に適している
  2. Simple Scalable

    • 中規模環境向け
    • コンポーネントをRead, Write, Backendに分離
  3. Distributed Mode

    • 本番環境向け
    • 各コンポーネントが独立して動作
    • スケーラビリティと可用性が高い

細かい構成については公式ドキュメントに詳しい説明が載っているのでそちらをご覧ください

インストール手順

ここからは、Lokiのインストールと基本的な利用方法について記載します!
以下のインストールでは,Distributed Modeを簡易的に実装する手順で説明します。
インストールについては、EKS上にHelmでデプロイすることを想定しています。

Helmリポジトリの追加

helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

設定ファイルの準備

以下の内容でvalues.yamlを作成します:
ログ保管に関してはまず、手始めにS3と互換性のあるMinioを利用しようと思います。
もちろんS3などのオブジェクトストレージに変更も可能で、S3への変更方法は後述します。

deploymentMode: Distributed
loki:
  auth_enabled: false
  commonConfig:
   ## ingesterのレプリカ数が1なので、replication_factorは1とします
    replication_factor: 1
  ## s3となってますがminioを有効にすることで、内部でminioを呼び出します。
  storage:
    type: s3

# Configuration for the gateway
gateway:
  enabled: false

# Configuration for the write pod(s)
write:
  replicas: 0
  
read:
  replicas: 0
  
backend:
  replicas: 0

# 各コンポーネントの設定,検証のためreplicasは1としています。
ingester:
  replicas: 1
  zoneAwareReplication:
    enabled: false

distributor:
  replicas: 1

querier:
  replicas: 1

queryFrontend:
  replicas: 1

queryScheduler:
  replicas: 1

indexGateway:
  replicas: 1

compactor:
  replicas: 1

ruler:
  enabled: true
  replicas: 1

# MinIO設定
minio:
  enabled: true
  replicas: 1
  persistence:
    size: 5Gi
    storageClass: gp2

Lokiのインストール

変更したvalues.yamlを指定して、helmコマンドでデプロイします。

# Lokiのインストール
helm upgrade --install loki grafana/loki \
  --create-namespace --namespace loki \
  -f values.yaml

以下のように全てRunningになれば起動完了です。

loki               loki-canary-b456t                                    1/1     Running     0          71m
loki               loki-canary-lc54g                                    1/1     Running     0          71m
loki               loki-canary-wxgdw                                    1/1     Running     0          71m
loki               loki-canary-xd7l7                                    1/1     Running     0          71m
loki               loki-chunks-cache-0                                  2/2     Running     0          71m
loki               loki-compactor-0                                     1/1     Running     0          36m
loki               loki-distributor-6567649c7f-kxj9d                    1/1     Running     0          36m
loki               loki-index-gateway-0                                 1/1     Running     0          33m
loki               loki-ingester-0                                      1/1     Running     0          36m
loki               loki-querier-645976dc89-2z4xc                        1/1     Running     0          36m
loki               loki-query-frontend-b75f8c997-44lqr                  1/1     Running     0          36m
loki               loki-query-scheduler-747778767f-l4qws                1/1     Running     0          36m
loki               loki-results-cache-0                                 2/2     Running     0          71m
loki               loki-ruler-0                                         1/1     Running     0          33m

ログ収集

ログを収集するために、KubernetesクラスタにFluent BitをDeamonsetでインストールし、KubernetesのログをLokiに送る設定をします。
導入については詳しくは説明しませんが、公式ドキュメントが参考になりそうなのでこちらを参考にしてください

以下の設定でlokiのdistributorにログを送ることができます。

[OUTPUT]
    Name loki
    Match kube.*
    Host loki-distributor.loki.svc.cluster.local
    Port 3100

ここまでで、GrafanaからLogが見れるようになっているはずです。お手元のGrafanaで確認してみてください。
Grafanaでログが確認できれば基礎は完了となります。続いて、少し実践的なことをやってみようと思います!
Log結果

アラート設定

Lokiのアラートを通知するためにalert-managerを利用します。
私の環境ではprometheusをインストールした際に、alert-managerもインストールしているためそれを利用します。

Lokiのアラートを設定するには、以下の手順で設定します

  1. ルールの定義: LogQLを使用してアラートルールを定義します
  2. アラートマネージャーの設定: 通知先の設定
  3. ルールのデプロイ: Kubernetes環境でのルール適用方法

ルールの定義

Lokiのログを検索するには、LogQL (Loki Query Language) と呼ばれるクエリ言語を使用します。LogQLはPromQLに似た構文を持ち、ログストリームの選択と、選択したストリーム内のログ行のフィルタリングを組み合わせてログを検索します。

LogQLのクエリは、基本的に以下の2つの部分から構成されます。

  • ログストリームセレクター:
    検索対象のログストリームをラベルを使って指定します。
    {label1="value1", label2="value2"} のように中括弧 {} で囲んで指定します。
  • ラインフィルター:
    選択したログストリーム内の各ログ行の内容をフィルタリングします。
    パイプ | の後に演算子と条件を指定します

これらを利用して、該当のログを検索することができます。
例:namespace=lokiかつlevel=errorという文字列を含む行のみを表示

{namespace="argocd"} |= `level=error`

また、上記のlog queriesを拡張してMetric queriesとして作成することで
カスタムメトリックをアラートとして定義できます。
例:過去5分間に10件以上発生した場合

count_over_time({namespace="argocd"} |= `level=error` [5m]) > 9

これで下記のようなアラートが完成します。
「argocd ネームスペースから出力されたログの中で、ログ行に level=error という文字列を含むものが、過去5分間に10件以上発生する場合」

次に、完了したアラート定義をLokiに組み込む必要があります。
ruleファイルの保存はlocalに保管しております。※内部的にはConfigMapで保管

Lokiのアラートルールは、Prometheusのルール形式と似ています。基本的な構造は以下の通りです。
localに保管するため,ruler.directoriesの下に記載してます。

ruler:
  enabled: true
  replicas: 1
  directories:
    tenant_foo:
      ## LokiのアラートルールをTextファイルとして設定してください
      rules1.txt: |
        groups:
          - name: should_fire
            rules:
              - alert: HighPercentageError
                expr: |
                  count_over_time({namespace="argocd"} |= `level=error` [5m]) > 9
                for: 1m
                labels:
                  severity: warning
                annotations:
                  summary: High error rate

アラートマネージャの設定

alertmanager_urlにalertmanagerのURLを設定することでアラートを送信することができます。

loki:
  rulerConfig:
    ## AlertmanagerのURLを設定してください
    alertmanager_url: http://prometheus-alertmanager.prometheus.svc.cluster.local:9093

    ## localに保管するために必要な設定となります
    storage:
      type: local
      local:
        directory: /etc/loki/rules
    rule_path: /tmp/loki

ログ保管場所の変更

今まで、MinIOを使用してデータを管理していますが、本番環境を想定してAWS S3への移行を実施してみたいと思います。

  1. S3バケットの作成
    S3バケットの作成については以下のように作成します。
## Lokiによるログ保管のためのバケット
module "s3_bucket" {
    source = "terraform-aws-modules/s3-bucket/aws"
    version = "4.6.0"

    bucket = "test-loki-logs-bucket"
    object_ownership = "BucketOwnerPreferred"

    versioning = {
        enabled = true
    }
}
  1. IAMロールの設定
    続いて、S3にLokiがアクセスできるとようにIRSAの設定をします。
resource "aws_iam_role" "loki_role" {
  name = "loki-irsa-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = "sts:AssumeRoleWithWebIdentity"
        Principal = {
          Federated = module.eks.oidc_provider_arn ##eks moduleを使っているのでこのように記載してますが、eksnodeのOidcを設定してください
        }
        Condition = {
          StringEquals = {
            "${replace(module.eks.cluster_oidc_issuer_url, "https://", "")}:sub": "system:serviceaccount:loki:loki" ##Lokiのサービスアカウント
            "${replace(module.eks.cluster_oidc_issuer_url, "https://", "")}:aud": "sts.amazonaws.com"
          }
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "loki_policy" {
  name = "loki-policy"
  role = aws_iam_role.loki_role.id

  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": [
          "s3:GetObject",
          "s3:PutObject",
          "s3:DeleteObject",
          "s3:ListBucket",
          "s3:GetBucketLocation",
          "s3:DeleteObject"
        ],
        "Resource": [
          "*"
        ]
      }
    ]
  })
}
  1. Lokiの設定変更
    values.yamlのloki.storageに以下のような設定に変更し、schemaConfigにてS3を設定します。
loki:
  storage:
    bucketNames:
      chunks: test-loki-logs-bucket
      ruler: test-loki-logs-bucket
      admin: test-loki-logs-bucket
    type: s3
    s3:
      region: ap-northeast-1
    
  schemaConfig:
    configs:
      - from: 2020-04-01
        store: tsdb
        object_store: s3
        schema: v13
        index:
          prefix: loki_index_
          period: 24h

serviceaccountを設定します。

serviceAccount:
  create: true
  annotations: 
    eks.amazonaws.com/role-arn: arn:aws:iam::<AccountID>:role/loki-irsa-role ##(注: <AccountID> はご自身のAWSアカウントIDに置き換えてください)

minioをfalseにします。minioをfalseにしないと、S3にうまくログを送ってくれませんでした。

minio:
  enabled: false

これで、helmをupgradeすればS3のバケット内にfake(デフォルトのテナントID)というディレクトリが作成され、ログがチャンクとして圧縮された状態で保管されることになります。

まとめ

この記事では、Grafana Lokiを使用したログ管理システムの構築と運用について基礎的な説明をしました。
Alertなどの管理や、ログの保管場所など内部をよく理解しないと動きがわからないところがあり苦労しました。

次のステップとしては、AlertRuleのS3保管や、ログの削除ルールの追加(リテンション設定)などをやっていきたいと思います!
お疲れ様でした〜🎉

参考資料

GitHubで編集を提案

Discussion