🪵

Grafana Loki による AWS ログ監視基盤の構築

に公開

はじめに

初めまして、木村俊星 です。この度、某S社の SRE を専門とする事業部のインターンに参加しました。インターンでは、Grafana Loki によってログの監視基盤を構築する方法と、Amazon CloudWatch Logs と比較したときの得失について調査しました。本記事では、その調査結果を紹介します。

環境構築と調査結果を1つの記事に書くと長くなりすぎてしまうため、以下の2部構成としました。

  • 第1部
    • Grafana Loki について
    • AWS 検証環境の構築
  • 第2部
    • 検索性の比較
    • アラート機能の比較
    • 金銭コストの比較

本記事は第1部の内容です。

調査の背景

まずこのテーマの背景を簡単にご説明します。

このテーマを作成された方の案件では、クラウドインフラとして AWS が採用されており、ほとんどのログが Amazon CloudWatch Logs に保存されています。CloudWatch Logs は取り込んだデータ量に応じて課金されるため、ログを気軽に保存できません。

コストを抑えるために、S3 にログを保存することも可能ですが、S3 単体では検索性やログの可視化・表現力に限界があるという欠点があります。S3 に保存したログを Athena や QuickSight といったサービスと組み合わせることで、ある程度柔軟な分析や可視化は可能になりますが、そのための仕組みを構築・運用するには手間がかかります。

「コストと検索性を両立できるいい感じのツールはないか」ということで、Grafana Loki について調査する本テーマが作成されました。

Grafana Loki について

Grafana Lokiとは

本節では Grafana Loki がどんなツールなのか紹介します。

Grafana Loki(以下、Loki と略す)とは Grafana Labs 社が開発している OSS のログ管理ツールです。以下の図は Loki を取り巻くログ監視基盤の全体像です。

Loki Logging Stack

この監視基盤において Loki が果たす役割は2つあります。

  • Agent からのログを指定されたストレージに保存する
  • Grafana や LogCLI からのクエリを処理する

上記2つの役割を担うという点で Prometheus と同じ立ち位置にあるツールですが、扱うデータとその収集方法に違いがあります。

Loki Prometheus
扱うデータ ログ メトリクス
収集方法 プッシュ型 プル型

Loki の特徴は、ログの保存方法を工夫することで低いストレージコストと高い拡張性を実現していることです。以降では、Loki がどうやってこれらを実現しているのか説明します。

低ストレージコストの秘訣

Loki はログ本体ではなく、ログに付与されたラベルの組に対してインデックスを張ることでストレージコストを抑えます。

ログの検索を高速化するために、ログ自体にインデックスを張る場合を考えます。ログのような自然言語のデータに対してインデックスを張る時は全文インデックスがよく用いられます。

全文インデックスについては、技術評論社さんの 転置索引の詳細 が分かりやすいです。文書に登場する単語のリストを「辞書」に保存し、辞書の各単語が出現した文書のIDを「ポスティングリスト」に保存します。

転置索引の構成

全文インデックスによって、特定の単語が出現した文書を高速に求めることができますが、インデックス対象のデータよりもインデックス自体のサイズが大きくなることが多く、より多くの容量を消費します。

Loki はログではなくログに付与されているラベルの組に対してインデックスを張ります。そのために、あらかじめどのログに対してどんなラベルを付与するか Agent 側で定義しておきます。Agent は定義されたラベルをログに付与し、Loki へと送信します。Agent からログを受信した Loki は、以下の図のようにログを保存します。

Data Format

  1. ログに付与されているラベルからハッシュ値 (Stream ID)を求める
  2. ログ自体は「チャンク」と呼ばれる単位に圧縮し、ストレージに保存する
  3. 1で求めたハッシュ値をキーとするポスティングリストに、チャンクが保存された位置を特定できる情報を保存する

ログ自体をインデックスする場合と比べてインデックスのサイズが小さくなるため、必要な容量を抑えることができます。ラベルにしなかった値でログを検索することはできないため、検索性を犠牲にしてストレージコストを抑えるアプローチと言えます。

ただし、ラベルの設計を誤ると逆にストレージコストが膨れ上がります。Lokiでは、ラベルとその値の全ての組み合わせを「ストリーム」と呼ばれる単位で区別して管理します。例えば、ログに methodstatus というラベルを付与する場合において、method が4種類の値をとり、status が6種類の値をとるならば、全部で4 × 6 = 24個のストリームが管理されます。そして、このストリーム毎にインデックスも管理されるため、とり得る値が多い属性をラベルにすると、インデックスがいくつも作成され、容量が増えてしまうだけでなく、インデックスの管理に伴うオーバーヘッドが増大します。Loki の低ストレージコストの恩恵に与るには、とり得る値が多い属性をラベルにしてはいけません。そのような場合には、ラベルと同様に扱うための「構造化メタデータ」を使用します。詳しくは 公式ドキュメント をご参照ください。

高い拡張性の秘訣

Loki は複数のマイクロサービスによって構成されており、それぞれのコンポーネントが独立してスケールアウトすることで、負荷の変動に対応できるように設計されています。以下の図は Loki の代表的なコンポーネントを図示したものです。

Loki Architecture

Loki のコンポーネントはログの書き込みに関わるコンポーネントと読み出しに関わるコンポーネントに大別されます。それぞれのコンポーネントがどのように動作するかは GO さんの Grafana Lokiでログを検索 で丁寧に解説されているので、こちらをお読みください。負荷に応じて Loki 内部でこれらのコンポーネントが独立してスケールアウトします。

さらに、Loki にはデプロイのモードが3つ用意されており、求められる監視基盤の規模に応じて拡張性を選択できます。

モード 動作方法 拡張性
Monolithic mode 全てのコンポーネントを単一のプロセスで動作させる
Simple scalable read, write, backendの3種類に分けて別々のプロセスで動作させる
Microservice mode 各コンポーネントを別々のプロセスで動作させる

Loki はログ本体とログを管理するソフトウェアを分離しているので、デプロイのモードを途中で変えたい時に、ほとんど設定を変えることなく移行できると ドキュメント には書かれています(これについては検証していません)。

以上の機能によって、コンポーネントの数と独立性を調整しながら、負荷に応じて柔軟にスケールできるのが Loki の強みです。

AWS 検証環境の構築

本節では前節で解説した Loki と CloudWatch Logs を比較するための検証環境を構築します。まず検証環境の構成と構築に利用するツールを説明し、実際にツールを使って検証環境を構築した後、最後に Grafana と CloudWatch Logs からクエリを実行してみます。

検証環境の説明

今回構築する環境を以下に図示します。

検証環境

ログを出力するアプリには Google Cloud が公開している以下のデモ用アプリを使用します。

https://github.com/GoogleCloudPlatform/microservices-demo

README では GKE にデプロイする手順が説明されていますが、Kubernetes のマニフェストが提供されているので、AWS EKS にもデプロイできます。このアプリが出力するログを Alloy と Fluent Bit で拾って、それぞれ Loki と CloudWatch に保存します。Loki に保存されたログを Grafana から検索し、CloudWatch に保存されたログを CloudWatch Logs から検索します。

構築に使用するツール

今回は検証環境の構築に eksctl と Terraform を以下の用途で使います。

ツール 用途
eksctl EKSクラスターの構築
Terraform EKSクラスター上にリソースを構築

EKS クラスターの構築にだけ eksctl を使っている理由は、Terraformで構築するよりも工数が少なそうだったからです。

元々 eksctl と aws コマンドのみで検証環境を構築していたのですが、毎回同じコマンドを同じ順序で実行するのが面倒になってきたので、Terraform を導入したという経緯があります。最初は Terraform で全て構築するつもりでしたが、EKSクラスターを Terraform で構築する場合は、EKS だけでなく VPC のサブネットまで自前で構築しないといけないことが分かり、それならすでに完成している eksctl を使う方が効率的だと考えました。

Kubernetes リソースを YAML で管理しなかった理由は、Terraform に挑戦してみたかったからです。実際に Terraform を使ってみて開発体験はとても良かったです。個人的に利点だと感じたことを以下に書きます。

  • 環境変数を terraform.tfvars で上書きできる
    → 同じ Terraform ファイルで複数の環境に対応できる
  • templatefile() で YAML に値を埋め込める
    → 環境を変える時に YAMLを書き換えなくていい
  • terraform apply/destroy だけで手軽に環境を構築 / 破壊できる
  • depends_on で依存関係を定義できる
    → コマンドを実行する順番を気にしなくていい

サンプルリポジトリ

いよいよ eksctl と Terraform を使って検証環境を構築していきます。本節で使用するファイルを保存したリポジトリをご用意したので、実際に動かしてみたい方はご活用ください。

https://github.com/kimurash/microservices-log-monitoring-demo

EKS クラスターの構築

eksctl を使って EKS クラスターを構築するときはYAML 形式のクラスター設定ファイルが必要です。今回使用するクラスター設定ファイルの内容を以下に示します。

cluster-config.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: loki-experiment
  region: ap-northeast-1
  version: "1.33" # Kubernetes のバージョン

addons:
  # PersistentVolumeClaim (PVC) を作成すると自動的に EBS ボリュームが作成されるようになる
  - name: aws-ebs-csi-driver
  # Pod が起動したときに自動的に IAM ロールをアタッチする
  - name: eks-pod-identity-agent

nodeGroups:
  - name: main
    instanceType: "t3.large"
    desiredCapacity: 3
    minSize: 3
    maxSize: 5
    amiFamily: AmazonLinux2023
    iam:
      # EBS CSI Driver が EBS ボリュームを操作するために必要な IAM ポリシーをノードの IAM ロールに自動でアタッチする
      withAddonPolicies:
        ebs: true
    volumeSize: 80 # [GiB]
    volumeType: gp3
    # EC2インスタンスとEBS ボリューム間のデータ通信専用にネットワーク帯域を確保する
    ebsOptimized: true

このファイルは Deploy the Loki Helm chart on AWS を参考にして書きました。今回使用しているアドオンについて以下で理由を補足します。

今回は2つのEKS アドオンを使用しています。

1つ目は EBS CSI Driver です。このアドオンは、Podが永続ストレージを要求した(PersistentVolumeClaim を出した)時に、自動的にEBS ボリュームを作成し、Pod が稼働しているノード(EC2 インスタンス)にアタッチしてくれます。今回は Loki がまだストレージに書き込まれていないチャンクを保存しておくための永続ストレージを必要とするため、このアドオンを追加する必要があります。

2つ目のアドオンは Pod Identity Agent です。このアドオンは、Loki と Fluent Bit が それぞれ S3 と CloudWatch にアクセスするために必要です。この必要性を理解するには、まず EKS Pod Identity という機能について理解する必要があります。この機能についてはAPコミュニケーションズさんの EKS Pod Identityの仕組みを深堀りしてみた が分かりやすいので、こちらをお読みください。ざっくり言うと、特定のサービスアカウントに IAM ロールを自動で付与する機能です。Pod が AWS のサービスにアクセスするときに、Pod に代わって IAM から必要なトークンを取得する役割を担います。

先のクラスター設定ファイルを使って以下のコマンドでEKS クラスターを構築します。

eksctl create cluster -f cluster-config.yaml

実際に計測したことはありませんが、EKS クラスターの構築にはかなり時間がかかります。私の環境では少なくとも5分はかかっていると思います。ログの出力が止まったからといって中断せず、気長に実行が終了するのを待ちましょう。

同じ関心事は同じ場所に書いてあった方がいいと思うので、EKSクラスターを削除するコマンドもここに示しておきます。

eksctl delete cluster -f cluster-config.yaml --disable-nodegroup-eviction

--disable-nodegroup-eviction は EBS CSI Driver がインストールされているクラスターを削除するときにつける必要があるようです。

このアドオンがインストールされているクラスターでは Pod Disruption Budget(PDB)ポリシーを持つ Pod が2つ稼働しています。PDB とは Pod が kubectl drain などによって意図的に停止させられた時に、最低限稼働している Pod の数を保証する機能です。このポリシーを無効化するために上記のオプションをつけています。

Terraform モジュール

予告した通り、リソースの構築には Terraform を使います。今回は Terraform のモジュールを以下の4つに分けました。

  • AWS リソース
  • デモ用アプリ
  • Grafana スタック
  • Fluent Bit

AWS リソースのデプロイ

本節でデプロイする AWS のリソースを以下に整理します。

# リソースの種類 説明
1 S3 バケット Loki がインデックスとチャンクを保存しておくためのバケット
2 S3 バケット Loki の Ruler がルールを保存しておくためのバケット
3 IAM ポリシー Loki が S3 にアクセスするためのポリシー
4 IAM ロール Loki が S3 にアクセスするためのロール
5 IAM ロール Fluent Bit が CloudWatch にアクセスするためのロール
6 EKS Pod Identity Association Loki のサービスアカウントに4行目のIAM ロールを自動で割り当てる
7 EKS Pod Identity Association Fluent Bit のサービスアカウントに5行目の IAM ロールを自動で割り当てる

EKS Pod Identity Association とは先に説明した EKS Pod Identity の設定であり、

  • どの namespace の
  • なんというサービスアカウントに
  • どの IAM ロールを割り当てるか

を定めます。

上記のリソースをデプロイするための Terraform ファイルは terraform/aws にあります。ディレクトリ構成はこんな感じです。

.
├── iam.tf
├── providers.tf
├── s3.tf
├── terraform.tfvars
└── variables.tf

apply してリソースを構築します。

$ terraform apply

Apply complete! Resources: 9 added, 0 changed, 0 destroyed.

AWS のコンソールからリソースを構築できているか確認しておきましょう。

デモ用アプリのデプロイ

デモ用アプリのマニフェストは release/ にあります。このマニフェストを Kubectl Provider を使ってデプロイします。Terraform ファイルは terraform/app にあります。ディレクトリ構成はこんな感じです。

.
├── data.tf
├── main.tf
├── outputs.tf
├── providers.tf
├── terraform.tfvars
└── variables.tf

apply してデモ用アプリをデプロイします。

$ terraform apply

Apply complete! Resources: 36 added, 0 changed, 0 destroyed.

Outputs:

app_domain = "a6db64e33ab8149e787e2138dc61e21c-449915145.ap-northeast-1.elb.amazonaws.com"

apply 後に出力されるドメインにポート番号80でアクセスするとトップページが表示されます。

デモ用アプリ

Grafana スタックのデプロイ

本節でデプロイするリソースを以下に整理します。

アプリ デプロイ方法
Grafana Loki Helm チャート
Grafana Alloy Helm チャート
Grafana Kubernetes マニフェスト

Grafana だけデプロイ方法が違うのは Grafana の公式ドキュメント を参考にしたためです。以降ではそれぞれのコンポーネントの設定内容を示します。

Grafana Loki

デプロイ方法は Deploy the Loki Helm chart on AWS を参考にしました。

Loki の values.yaml には Terraform の variables.tf で定義している値が含まれており、variables.tf を更新すると values.yaml にも反映されるようにしたかったため、Terraformの templatefile 関数を使い、variables.tf の値をテンプレート内に埋め込みました。テンプレートファイルの拡張子は tftpl が推奨されているため、拡張子を変更しました。

values.tftpl
loki:
  schemaConfig:
    configs:
      - from: "2024-04-01"
        store: tsdb
        object_store: s3
        schema: v13
        index:
          prefix: loki_index_
          period: 24h
  storage_config:
    aws:
      region: ${aws_region}
      bucketnames: ${loki_chunks_bucket_name}
      s3forcepathstyle: false
    ingester:
      chunk_encoding: snappy
    pattern_ingester:
      enabled: true
    limits_config:
      allow_structured_metadata: true
      volume_enabled: true
      retention_period: 24h
      max_query_series: 1000
    compactor:
      retention_enabled: true
      delete_request_store: s3
    ruler:
      enable_api: true
      storage:
        type: s3
        s3:
          region: ${aws_region}
          bucketnames: ${loki_ruler_bucket_name}
          s3forcepathstyle: false
      # alertmanager_url: <URL> # The URL of the Alertmanager to send alerts (Prometheus, Mimir, etc.)
    querier:
      max_concurrent: 4
    storage:
      type: s3
      bucketNames:
        chunks: "${loki_chunks_bucket_name}"
        ruler: "${loki_ruler_bucket_name}"
        # admin: "<s3 bucket name>" # GEL customers only
      s3:
        region: ${aws_region}
        insecure: false
        s3forcepathstyle: false

serviceAccount:
  create: true
  name: loki

deploymentMode: SimpleScalable

backend:
  replicas: 2
  maxUnavailable: 1
  persistence:
    storageClass: gp3
read:
  replicas: 2
  maxUnavailable: 1
  persistence:
    storageClass: gp3
write:
  replicas: 3 # To ensure data durability with replication
  maxUnavailable: 2
  persistence:
    storageClass: gp3

# This exposes the Loki gateway so it can be written to and queried externaly
gateway:
  service:
    type: LoadBalancer
  basicAuth:
    enabled: true
    existingSecret: loki-basic-auth

# Since we are using basic auth, we need to pass the username and password to the canary
lokiCanary:
  extraArgs:
    - --pass=$(LOKI_PASS)
    - --user=$(LOKI_USER)
  extraEnv:
    - name: LOKI_PASS
      valueFrom:
        secretKeyRef:
          name: canary-basic-auth
          key: password
    - name: LOKI_USER
      valueFrom:
        secretKeyRef:
          name: canary-basic-auth
          key: username

chunksCache:
  # Amount of memory allocated to chunks-cache for object storage (in MB).
  allocatedMemory: 1024

# Enable minio for storage
minio:
  enabled: false

Deploy the Loki Helm chart on AWS で示されている YAML を検証環境用に書き換えました。書き換えた部分を全て示すのは大変なので、特に重要な項目を以下にまとめました。

変更項目 説明 変更前 変更後 変更理由
deploymentMode デプロイのモード Distributed SimpleScalable 検証が目的なのでそんなに高い拡張性は必要ない
loki.limits_config.retention_period ログを保持する期間 672h (28日間) 24h (1日間) 長期間ログを保存しておく必要はない
loki.limits_config.max_query_series 1回のクエリで出力できるストリーム数の上限 なし 1000 クエリを実行するときのエラーを防ぐため
chunksCache チャンクのキャッシュに使用するメモリ容量 なし allocatedMemory: 1024 を追加 メモリ消費量を抑えるため
serviceAccount.annotations 省略 IRSA の ARN 削除 今回はIRSAではなくPod Identity を使用するため
serviceAccount.name 省略 なし loki Pod Identity によってサービスアカウントに確実にロールを付与するため

Grafana Alloy

デプロイ方法は Collect Kubernetes logs and forward them to Loki を参考にしました。Alloy 自体の設定ファイル(config.alloy)と Alloy を Helm チャートでデプロイするための values.yaml を用意します。

config.alloy では「どこからログを収集して」「どのように加工して」「どこに送信するか」を Terraform の HCL 風の構文で記述します。

config.alloy
loki.write "default" {
  endpoint {
    url = "http://loki-write.grafana.svc:3100/loki/api/v1/push"
    tenant_id = "default"

    basic_auth {
      username = "loki"
      password = "loki"
    }
  }

  headers = {
    "X-Scope-OrgID" = "default",
  }
}

/* System logs */
loki.source.journal "node_journal" {
  path = "/var/log/journal"

  labels = {
    job = "node/journal",
    cluster = "loki-experiment",
    node_name = sys.env("HOSTNAME"),
  }

  forward_to = [loki.write.default.receiver]
}

/* Pods logs */
// discovery.kubernetes allows you to find scrape targets from Kubernetes resources.
// It watches cluster state and ensures targets are continually synced with what is currently running in your cluster.
discovery.kubernetes "pod" {
  role = "pod"
  // Restrict to pods on the node to reduce cpu & memory usage
  selectors {
    role = "pod"
    field = "spec.nodeName=" + coalesce(sys.env("HOSTNAME"), constants.hostname)
  }
}

// discovery.relabel rewrites the label set of the input targets by applying one or more relabeling rules.
// If no rules are defined, then the input targets are exported as-is.
discovery.relabel "pod_logs" {
  targets = discovery.kubernetes.pod.targets

  // Label creation - "namespace" field from "__meta_kubernetes_namespace"
  rule {
    source_labels = ["__meta_kubernetes_namespace"]
    action        = "replace"
    target_label  = "namespace"
  }

  // Label creation - "pod" field from "__meta_kubernetes_pod_name"
  rule {
    source_labels = ["__meta_kubernetes_pod_name"]
    action        = "replace"
    target_label  = "pod"
  }

  // Label creation - "container" field from "__meta_kubernetes_pod_container_name"
  rule {
    source_labels = ["__meta_kubernetes_pod_container_name"]
    action        = "replace"
    target_label  = "container"
  }

  // Label creation - "app" field from "__meta_kubernetes_pod_label_app_kubernetes_io_name"
  rule {
    source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"]
    action        = "replace"
    target_label  = "app"
  }

  // Label creation - "job" field from "__meta_kubernetes_namespace" and "__meta_kubernetes_pod_container_name"
  // Concatenate values __meta_kubernetes_namespace/__meta_kubernetes_pod_container_name
  rule {
    source_labels = ["__meta_kubernetes_namespace", "__meta_kubernetes_pod_container_name"]
    action        = "replace"
    target_label  = "job"
    separator     = "/"
    replacement   = "$1"
  }

  // Label creation - "__path__" field from "__meta_kubernetes_pod_uid" and "__meta_kubernetes_pod_container_name"
  // Concatenate values __meta_kubernetes_pod_uid/__meta_kubernetes_pod_container_name.log
  rule {
    source_labels = ["__meta_kubernetes_pod_uid", "__meta_kubernetes_pod_container_name"]
    action        = "replace"
    target_label  = "__path__"
    separator     = "/"
    replacement   = "/var/log/pods/*$1/*.log"
  }

  // Label creation - "container_runtime" field from "__meta_kubernetes_pod_container_id"
  rule {
    source_labels = ["__meta_kubernetes_pod_container_id"]
    action        = "replace"
    target_label  = "container_runtime"
    regex         = "^(\\\\S+):\\\\/\\\\/.+$"
    replacement   = "$1"
  }
}

// loki.source.kubernetes tails logs from Kubernetes containers using the Kubernetes API.
loki.source.kubernetes "pod_logs" {
  targets    = discovery.relabel.pod_logs.output
  forward_to = [loki.process.pod_logs.receiver]
}

// loki.process receives log entries from other Loki components, applies one or more processing stages,
// and forwards the results to the list of receivers in the component's arguments.
loki.process "pod_logs" {
  stage.static_labels {
    values = {
      cluster = "loki-experiment",
    }
  }

  forward_to = [loki.write.default.receiver]
}

/* Kubenetes Cluster Events */
// loki.source.kubernetes_events tails events from the Kubernetes API and converts them
// into log lines to forward to other Loki components.
loki.source.kubernetes_events "cluster_events" {
  job_name   = "integrations/kubernetes/eventhandler"
  log_format = "logfmt"
  forward_to = [
    loki.process.cluster_events.receiver,
  ]
}

// loki.process receives log entries from other loki components, applies one or more processing stages,
// and forwards the results to the list of receivers in the component's arguments.
loki.process "cluster_events" {
  forward_to = [loki.write.default.receiver]

  stage.static_labels {
    values = {
      cluster = "loki-experiment",
    }
  }

  stage.labels {
    values = {
      kubernetes_cluster_events = "job",
    }
  }
}

ドキュメントに掲載されている内容を検証環境向けに書き換えました。

  • プレースホルダーの部分を検証環境の値に書き換えた
  • loki.writeX-Scope-OrgID ヘッダーを追加した

以下の図は config.alloy の内容に従ってログが処理する流れを図示した UI です。

Graph Page

これは Alloy UI の実験的な機能であり、Alloyをデプロイした後にローカルからポートフォワーディングすると見ることができます。

$ kubectl port-forward -n grafana svc/alloy 8080:12345
Forwarding from 127.0.0.1:8080 -> 12345
Forwarding from [::1]:8080 -> 12345

3種類のログを収集して Loki へと送信している様子が示されています。

Alloy の values.yaml を以下に示します。

values.yaml
alloy:
  configMap:
    create: false
    name: alloy-config
    key: config.alloy

config.alloy の内容を values.yaml に直書きすることもできるようですが、YAMLの中に YAML 以外の構文を混ぜたくなかったので、Alloy 自体の設定は ConfigMap としてデプロイし、values.yaml には ConfigMap を参照するための名前を書きました。

Grafana

デプロイ方法は Deploy Grafana on Kubernetes を参考にしました。

ドキュメントに掲載されているマニフェストをそのまま利用したので、改めてここにマニフェストの内容は貼りません。

デプロイ

Grafana スタックをデプロイするためのTerraform ファイルは terraform/grafana にあります。ディレクトリ構成はこんな感じです。

.
├── alloy.tf
├── config.alloy
├── data.tf
├── grafana.tf
├── loki.tf
├── main.tf
├── manifests
│   ├── alloy
│   │   └── values.yaml
│   └── grafana
│       └── grafana.yaml
├── outputs.tf
├── providers.tf
├── templates
│   └── loki
│       └── values.tftpl
├── terraform.tfvars
└── variables.tf

loki.tf にSecret が2つありますが、loki_basic_auth は Loki が Basic 認証するときに参照する Secretで、 loki_canary_auth は Loki Canary が Loki にアクセスするときに参照する Secret です。

loki_basic_auth で使用されている.htpasswd というファイルは以下のコマンドで作成することができます。

htpasswd -c .htpasswd <username>

今回はユーザー名もパスワードも「loki」 とします。

htpasswd -c .htpasswd loki

パスワードの入力を求められるので同じく「loki」 と入力すると、ユーザー名とパスワードを MD5 でハッシュ化した文字列を : で連結した文字列が書き込まれた .htpasswd というファイルがカレントディレクトリに作られます。

apply して Grafana スタックをデプロイします。

$ terraform apply

Apply complete! Resources: 10 added, 0 changed, 0 destroyed.

Outputs:

grafana_domain = "a5e6e172338b84f7781a5fb9670bb837-2027701938.ap-northeast-1.elb.amazonaws.com"
loki_domain = "a20ecec24613c4f70b3deb37d1c11e26-782906181.ap-northeast-1.elb.amazonaws.com"

動作確認

実際に Grafana から Loki にクエリを送ってみます。apply 後に出力される Grafana のドメインにポート番号3000でアクセスするとログインページが表示されます。

Grafana

ユーザー名とパスワードに「admin」 と入力してログインすると、新しいパスワードを設定するよう求められるため、独自のパスワードを入力するとトップページへと遷移します。

Grafana

Grafana から Loki にクエリを送るには、まず Loki の Data Source を追加する必要があります。左のメニューから「Connections」のトグルを開いて 「Data Sources」を選択すると、Data Source の一覧画面が表示されます。

Grafana

「Add data source」を選択すると、Data Source の種類を選ぶ画面に遷移します。Loki を選択すると、Data Source の情報を入力するページが表示されます。

Grafana

今回は以下の3つの情報を入力します。

Loki Gateway の URL

一般的に同じ Kubernetes クラスター内の Service には以下の形式のURLでアクセスできます。

http://<service_name>.<namespace>.svc.cluster.local

Loki と Grafana は同じクラスター内にいるため以下のURLでGrafana から Loki Gateway の Service にアクセスできます。

http://loki-gateway.grafana.svc.cluster.local:80

Connection > URL に上記のURLを入力します。

Basic認証のユーザー名とパスワード

loki.tf でユーザー名とパスワードを共に「loki」にしているので、Grafana でもこの設定に合わせる必要があります。Authentication > Authentication methods で「Basic authentication」を選択し、ユーザー名とパスワードに「loki」と入力します。

HTTPヘッダー

今回は Loki を Simple Scalable モードでデプロイしているため、Loki に HTTP リクエストを送る時は X-Scope-OrgID というヘッダーでテナントIDを指定する必要があります。config.alloy でログを保存するときのテナントIDを default にしているので、Grafana でもこの設定に合わせる必要があります。Authentication > HTTP headers のトグルを開き、「Add header」を選択してヘッダー名と値を入力します。

画面の一番下までスクロールして「Save & test」を押します。「Data source successfully connected.」と表示されたら接続成功です。

Grafana

左のメニューから「Explore」を選択すると、最初から Data Source に Loki が選択された画面が表示されます。Data Sourceに Loki が選択されていない場合は、上部やや左のトグルを開いて Loki を選択してください。

Grafana

試しにフロントエンドの Pod からの INFO レベルのログを検索してみましょう。「Label browser」 から Pod の名前を調べて以下のようなクエリを実行します。

{pod="frontend-76dbbddfc5-2827x"} |= "info"

このクエリを実行した結果を以下に示します。

Grafana

Fluent Bit のデプロイ

Set up Fluent Bit as a DaemonSet to send logs to CloudWatch Logs を参考にしました。

このドキュメントでは Fluent Bit を DaemonSet としてデプロイします。マニフェストは350行ぐらいあるので、ここには貼りません。公開しているリポジトリの fluent-bit.yaml をご参照ください。

ドキュメントに沿って進めるときに注意点が1つあります。ドキュメントで使用されているマニフェストは Fluent Bit のバージョンが古く、EKS Pod Identity が正しく動作しません。クラスメソッドさんの aws-for-fluent-bit が EKS Pod Identity を利用したセットアップに対応したので試してみた にあるように、Fluent Bit のバージョンを「2.32.4」から「2.32.5」に上げる必要があります。

spec:
  template:
    spec:
      containers:
        - name: fluent-bit
          image: public.ecr.aws/aws-observability/aws-for-fluent-bit:2.32.5 # ここを変更する

Fluent Bit をデプロイするための Terraform ファイルは terraform/fluent-bit にあります。ディレクトリ構成はこんな感じです。

.
├── data.tf
├── main.tf
├── manifests
│   └── fluent-bit
│       └── fluent-bit.yaml
├── providers.tf
├── terraform.tfvars
└── variables.tf

apply して Fluent Bit をデプロイします。

$ terraform apply

Apply complete! Resources: 7 added, 0 changed, 0 destroyed.

AWS のコンソールから CloudWatch を開いてログが保存されていることを確認しましょう。以下のロググループにログが保存されているか確認します。

/aws/containerinsights/loki-experiment/application

CloudWatch

試しに簡単なクエリ実行してみましょう。「Logs Insightsで表示」を選択するとクエリを実行できる画面に遷移します。Grafana と同じくフロントエンドのPod からの INFO レベルのログを検索してみましょう。

filter kubernetes.pod_name like /frontend-.+/
| filter strcontains(@message, "info")

このクエリを実行した結果を以下に示します。

CloudWatch

おわりに

今回は AWS に Grafana Loki を使ったログ監視基盤と Fluent Bit で CloudWatch に同じログを保存する
仕組みを構築しました。第2部 ではこの環境を使って Grafana スタックと CloudWatch を以下の観点から比較します。

  • 検索性
  • アラート機能
  • 金銭コスト

ここまでお読みいただき、ありがとうございました!

Discussion