みてね Tech Blog
⚙️

ログ基盤をPromtailからGrafana Alloyに移行する

に公開

家族アルバム みてねでSREをしているyktakaha4と申します🐧

https://mitene.us/

本記事では、みてねのコンテナログ転送基盤をPromtailからGrafana Alloyへ移行した際に検討したポイントを整理します
参考になる部分があれば幸いです

Promtailについて

PromtailGrafana Loki向けに設計されたログ収集エージェントで、各ノードやコンテナからログを取得し、整形・ラベル付けをおこなってLokiに送信します
KubernetesにおいてはDaemonSetとしてデプロイすることで、クラスタ全体のログを効率的に収集することができます

https://grafana.com/docs/loki/latest/send-data/promtail/

みてねではインフラとしてAmazon EKSを採用しており、アプリケーションが出力するコンテナログをPromtailで収集してLokiに転送し、Grafanaでログの可視化や検索をおこなっています

オブザーバビリティの全体像については以下の記事に詳しくまとまっていますので、よければご覧ください

https://gihyo.jp/article/2023/03/mitene-06observability

便利に利用していたPromtailですが、後述するGrafana Alloyの登場によりGrafana Agentが非推奨化されたことに伴って、2025/02/13にLTSへ移行し、2026/03/02にEOL予定と発表されています
ログ基盤は日々の開発や調査に欠かせないもののため、公式に推奨されているAlloyへの移行を検討し始めました

https://github.com/grafana/loki/pull/16227

Grafana Alloyについて

Grafana Alloyは、メトリクス・トレース・ログといった観測データを一元的に収集・転送できるOSSです
従来提供されていたGrafana Agentと比較してOpenTelemetry Collector互換であることが謳われており、Alloyを導入するだけで様々なユースケースに対応できます

https://grafana.com/blog/2024/04/09/grafana-agent-to-grafana-alloy-opentelemetry-collector-faq/

Alloyでは処理をComponentという単位で定義します。設定ファイルはHCL形式で記述され、コンポーネント間の依存関係を宣言的に定義できるようになっています

https://grafana.com/docs/alloy/latest/get-started/components/

本来、メトリクスやトレースの収集にも活用できますが、前述の通りみてねでは既に複数のツールを組み合わせてオブザーバビリティを実現しているため、今回は廃止されるPromtailの移行に絞って紹介していきます

少し詳細を見てみる

記事執筆にあたって理解を深めるため、AlloyとOpenTelemetryの関係性について少し調べてみました
(Alloy移行にあたって必須の知識ではないので、読み飛ばしても差し支えありません)

弊社で横断SREをしているYoshiiさんがOTel入門にちょうどいい文量のBookを執筆していたのでありがたかったです

https://zenn.dev/ryoyoshii/books/opentelemetry-firststep

なんとなく全体感が掴めたところで、公式が提供しているOTLPの仕様も読んでみます

https://opentelemetry.io/docs/specs/otel/logs/

Promtailから移行する私たちの構成はLegacy First-Party Applications Logsに該当するようです
Alloy(≒OpenTelemetry Collector)ネイティブの構成ではトレース情報などのコンテキストを付与してファイルを介さずログ転送できるようなので、今後検討していきたいところです


https://opentelemetry.io/docs/specs/otel/logs/ より引用

加えてログを表現するデータモデルや、クライアントからログを送信するためのProtocol Buffersが定義されていることも確認しました

https://github.com/open-telemetry/opentelemetry-proto/blob/v1.7.0/opentelemetry/proto/collector/logs/v1/logs_service.proto#L27-L32

https://github.com/open-telemetry/opentelemetry-proto/blob/8654ab7a5a43ca25fe8046e59dcd6935c3f76de0/opentelemetry/proto/logs/v1/logs.proto#L134-L136

Alloyにおいてはotelcol配下にOpenTelemetryに関連するコンポーネントがまとめられており、OTLPからLoki形式へのログ変換はotelcol.exporter.loki、その逆はotelcol.receiver.lokiによっておこなえます

https://grafana.com/docs/alloy/latest/reference/components/otelcol/

また、前述したgRPCサーバを起動するにはotelcol.receiver.otlpコンポーネントを利用するようです

https://grafana.com/docs/alloy/latest/reference/components/otelcol/otelcol.receiver.otlp/

いずれも今回直接的に利用する機能ではありませんが、AlloyがどのようにOTelとの互換性を実現しているか、なんとなく理解できたように思います

実装方式

話を戻して、移行時に参考にした情報についてご紹介します

公式から提供されている移行方法としてはMigrate from Promtail to Grafana Alloyというドキュメントがありますが、設定ファイルの移行方法の説明が中心でこれだけだと全体感が掴みづらいです

https://grafana.com/docs/alloy/latest/set-up/migrate/from-promtail/

ログ転送に関する設定はCollect Kubernetes logs and forward them to Lokiで詳しく紹介されています

https://grafana.com/docs/alloy/latest/collect/logs-in-kubernetes/

より包括的な構成を理解するにはDeploy Grafana Alloyという節の情報が役立ちます
デプロイ方式について、メリット・デメリットを比較しながら複数紹介されています

https://grafana.com/docs/alloy/latest/set-up/deploy/

公式からHelm Chartも提供されているため、利用するとよさそうです

https://artifacthub.io/packages/helm/grafana/alloy

これらを踏まえて実装をおこなうのですが、デプロイやログ転送方式の組み合わせによって様々な構成を取れるため、少々混乱する場面がありました

本記事では移行の際の叩き台として使えるよういくつかパターンを提示しますので、ご自身のユースケースに併せて検討いただければと思います🐏

① DaemonSet + loki.source.file

Promtailと同様にAlloyをDaemonSetとして配置し、各Nodeのログファイルから収集をおこなう構成です
最終的にみてねで選択した方式になります

Helm Chartで特に必要となる設定を示します

https://artifacthub.io/packages/helm/grafana/alloy

values.yaml
alloy:
  # Alloyの設定ファイルを独自に管理
  configMap:
    create: false
    name: alloy-config
    key: config.alloy
  # AlloyのPodにNodeの /var/log/* をマウント
  mounts:
    varlog: true

controller:
  # DaemonSetとして配置
  type: daemonset
templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: alloy-config
data:
  config.alloy: |
    // ここにHCLを書いていく

config.alloy において重要な設定項目を示します

config.alloy
// 書き込み先のLokiバックエンドを定義
loki.write "default" {
  endpoint {
    // Lokiのエンドポイントを指定
    url = "http://<loki-address>/loki/api/v1/push"
  }
}

// Kubernetes APIからPod情報を取得
discovery.kubernetes "pod" {
  role = "pod"

  // DaemonSetと同じNodeのPodを選択
  // ref: https://github.com/grafana/alloy/issues/1217#issuecomment-2236272320
  selectors {
    role  = "pod"
    field = "spec.nodeName=" + coalesce(env("HOSTNAME"), constants.hostname)
  }
}

// 取得したPod情報にラベルを追加
discovery.relabel "pod_logs" {
  targets = discovery.kubernetes.pod.targets

  // __path__ にPodに対応するNode上のログのパスを設定
  rule {
    source_labels = ["__meta_kubernetes_pod_uid", "__meta_kubernetes_pod_container_name"]
    action = "replace"
    target_label = "__path__"
    separator = "/"
    replacement = "/var/log/pods/*$1/*.log"
  }

  // その他必要なラベルを定義する
  // ref: https://grafana.com/docs/alloy/latest/collect/logs-in-kubernetes/#pods-logs
}

// __path__ ラベルを元にAlloyのPod内のパスを探索
local.file_match "pod_logs" {
  path_targets = discovery.relabel.pod_logs.output
}

// 見つかったファイルのログエントリをLokiコンポーネントに転送
loki.source.file "pod_logs" {
  targets    = local.file_match.pod_logs.targets
  forward_to = [loki.process.pod_logs.receiver]
}

// ログエントリを加工しLokiバックエンドに転送
loki.process "pod_logs" {
  // 固定ラベルを設定する例
  stage.static_labels {
      values = {
        cluster = "<cluster-name>",
      }
  }
  forward_to = [loki.write.default.receiver]
}

loki.source.file はファイルのtailに grafana/tail というライブラリを利用しており、これは従来のPromtailと同等のようです
(リポジトリは既にArchiveされているため、今後独自の実装に移行していく想定なのかもしれません)

https://github.com/grafana/alloy/blob/5da139b3e0496ac5aacf4d2c30fe9cd301502d14/internal/component/loki/source/file/tailer.go#L204-L215

https://github.com/grafana/loki/blob/4c88be0ef2310406206c02d7747bf73b51c95bda/clients/pkg/promtail/targets/file/tailer.go#L63-L74

② Deployment + クラスタリング + loki.source.kubernetes

AlloyをDeploymentとして配置した上で、Kubernetes API経由で各Podのログを収集する方式です
①との比較を示します

  • 👍 /var/log/* のマウントが不要のため、非rootユーザーで運用できる
  • 👍 Alloyを実サービスと異なるインフラで運用できる(その際の構成として推奨されている
  • 👎 ログ収集の過程で①の構成では生じないネットワークI/Oが発生する
  • 👎 ログの量が時間によって増減する場合、Alloyのオートスケーリングなどの対策をおこなう必要があり実装が複雑化する

Helm Chartの設定は以下です

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

  clustering:
    # Alloyの `--cluster.enabled` オプションの有効化
    # ref: https://grafana.com/docs/alloy/latest/reference/cli/run/#clustering
    enabled: true

controller:
  # Deploymentとして配置
  type: deployment
  # 固定台数で運用する場合
  replicas: 2
  # オートスケーリングをおこなう場合
  # ref: https://artifacthub.io/packages/helm/grafana/alloy#controller-autoscaling
  # autoscaling:
  #   horizontal:
  #     enabled: true

config.alloy の設定を示します

config.alloy
// 書き込み先のLokiバックエンドを定義
loki.write "default" {
  endpoint {
    // Lokiのエンドポイントを指定
    url = "http://<loki-address>/loki/api/v1/push"
  }
}

// Kubernetes APIからPod情報を取得
discovery.kubernetes "pod" {
  role = "pod"
}

// 取得したPod情報にラベルを追加
discovery.relabel "pod_logs" {
  targets = discovery.kubernetes.pod.targets

  // その他必要なラベルを定義する
  // ref: https://grafana.com/docs/alloy/latest/collect/logs-in-kubernetes/#pods-logs
}

// discovery.kubernetes にて取得したPodのログをKubernetes APIで収集する
loki.source.kubernetes "pod_logs" {
  targets    = discovery.relabel.pod_logs.output
  forward_to = [loki.process.pod_logs.receiver]

  clustering {
    // ログ転送をおこなうPodをシャーディングにより分散する
    // ref: https://grafana.com/docs/alloy/latest/reference/components/loki/loki.source.kubernetes/#clustering
    enabled = true
  }
}

// ログエントリを加工しLokiバックエンドに転送
loki.process "pod_logs" {
  // 固定ラベルを設定する例
  stage.static_labels {
      values = {
        cluster = "<cluster-name>",
      }
  }
  forward_to = [loki.write.default.receiver]
}

ややこしい点として、Alloyそのものをクラスタリング構成で起動するオプションと、ログ転送の負荷分散をおこなうオプションはそれぞれ別に定義されています
loki.source.kubernetes.clustering.enabled の有効化をおこなわずに展開してしまうと、全てのAlloyから全てのPodのログをLokiへ転送する挙動になるため注意しましょう

https://grafana.com/docs/alloy/latest/reference/cli/run/#clustering

https://grafana.com/docs/alloy/latest/reference/components/loki/loki.source.kubernetes/#clustering

なお、loki.source.kubernetes におけるログのtailは kubernetes/client-go でおこなわれているようでした

https://github.com/grafana/alloy/blob/3d97e3a0e81c9d08695dfdc473809931ae808dcd/internal/component/loki/source/kubernetes/kubetail/tailer.go#L169-L174

③ DaemonSet + loki.source.kubernetes

基本的には①または②の構成を選択することが多いのではないかと思うのですが、DaemonSetを使いつつログの取得元はKubernetes APIにするという構成も選択可能です
①の構成をベースに、 local.file_matchloki.source.file を削除し loki.source.kubernetes に置き換えれば実現できます

④ Deployment + loki.source.kubernetes

ログの量が一定、かつ出力量が少なく単一のAlloyで賄える場合には、②のクラスタリング設定を無効化した状態で運用することも可能です

loki.source.kubernetes のデフォルトの挙動として、Podの起動時(含む再起動)にはKubernetes APIから全期間のログを再びtailして再転送するため、短時間のPod停止であれば欠損なくログを転送することができます
開発環境など、コストを最小限にしたいケースで有用な場合があるかもしれません

⑤ StatefulSet(未検証)

今回は検証しなかったのですが、Alloyをメトリクス収集に用いる場合は、バックエンドの書き込み遅延が発生した時のためにWAL(Write-Ahead Logging)を利用することができます
そのような構成においては、AlloyをStatefulSetで展開することも可能のようです

https://grafana.com/docs/alloy/latest/set-up/deploy/#use-kubernetes-statefulsets

ログ転送という文脈では、①および②でtailerの実装を確認したようにAlloyはステートレスに構成できるため、状態を考慮した実装は不要です(たぶん)

役立った機能

移行作業中に便利だった機能についてご紹介します

alloy convert コマンド

実装方式の節でも紹介しましたが、AlloyのCLIには convert というサブコマンドが用意されており、PromtailやPrometheusなどの設定ファイルをAlloyで読み込める形式に変換することができます

https://grafana.com/docs/alloy/latest/set-up/migrate/from-promtail/#convert-a-promtail-configuration

Dockerで利用する方法を示します

Terminal
docker run \
    -v $PWD:/tmp/alloy \
    --rm grafana/alloy convert \
    --source-format=promtail \
    --output=/tmp/alloy/alloy.config \
    /tmp/alloy/promtail.yaml

入力ファイルに変換できない設定があった場合は、以下のようにエラーが表示されます
--bypass-errors オプションを付与すれば、エラーを無視して結果を出力することが可能です

Error: (Error) limits_config is not yet supported in Alloy
(Warning) Alloy's metrics are different from the metrics emitted by Promtail. If you rely on Promtail's metrics, you must change your configuration, for example, your alerts and dashboards.

Prometheusメトリクス

公式のHelm ChartserviceMonitor.enabled を有効化することでPrometheusメトリクスを取得可能になります

対応中に役立ったAlloyのメトリクスについてご紹介します

メトリクス 用途
alloy_resources_process_cpu_seconds_total AlloyのCPU利用
alloy_resources_process_resident_memory_bytes Alloyのメモリ消費量
alloy_resources_machine_rx_bytes_total
alloy_resources_machine_tx_bytes_total
AlloyのネットワークI/O
loki_write_sent_bytes_total
loki_write_sent_entries_total
loki.write の送信バイト数、ログエントリ数
loki_process_dropped_lines_total loki.process がログをDropした数

上記に加えて、Lokiやkube-state-metricsから得られるメトリクスについても確認するとよさそうです

メトリクス 用途
loki_discarded_samples_total Lokiで破棄されたサンプル数
loki_request_duration_seconds_count{method!="gRPC", status_code=~"^[013-9].*"} LokiバックエンドのHTTPエラー
loki_request_duration_seconds_count{method="gRPC", status_code!="success"} LokiバックエンドのgRPCエラー
kube_pod_container_status_restarts_total{pod=~".*alloy.*"} AlloyのPodの再起動数
kube_pod_container_status_last_terminated_reason{reason="OOMKilled", pod=~".*alloy.*"} AlloyのOOM発生数

Alloy UIとlivedebuggingオプション

Alloyはデフォルトでデバッグ用のUIを備えており、Podの 12345 ポートにアクセスすると確認できます

https://grafana.com/docs/alloy/latest/troubleshoot/debug/

アクセスするにはport-forwardコマンドが便利です

Terminal
kubectl port-forward <alloy-pod-name> 12345:12345

デバッグUIのGraph画面では、Alloyで定義した各Componentの依存関係や、Component間でどの程度のトラフィックが発生しているか視覚的に確認できます


Graph画面

より詳細なログの転送状況を確認したい場合は、livedebuggingオプションを利用できます

https://grafana.com/docs/alloy/latest/reference/config-blocks/livedebugging/

設定ファイルで簡単に有効化できます

config.alloy
livedebugging {
  enabled = true
}

有効化すると、loki.process などの一部コンポーネントの動作状況について確認できるようになります


Live Debugging画面

表示されるデバッグ情報のサンプルを示します
以下のように入力されたログレコードごとに [IN][OUT] が出力され、loki.process でおこなわれた加工が適切か確認することができます

[IN]: timestamp: 2025-08-21T09:43:59.252914558Z, entry: 2025-08-21T09:43:59.180539494Z stdout F test message, labels: {}, structured_metadata: {}
[OUT]: timestamp: 2025-08-21T09:43:59.252914558Z, entry: 2025-08-21T09:43:59.180539494Z stdout F test message, labels: {cluster="main"}, structured_metadata: {}

発生した挙動

移行作業中に遭遇した事象について、いくつかご紹介します

メモリ使用量の増加と転送遅延

Alloyにおけるリソースの使用量については、DaemonSetでLokiのログを転送する際の目安として以下の値が示されています

https://grafana.com/docs/alloy/latest/introduction/estimate-resource-usage/#loki-logs

As a rule of thumb, per each 1 MiB/second of logs ingested, you can expect to use approximately:

  • 1 CPU core
  • 120 MiB of memory

上述した程度のリソース使用量であれば一般的に気にする必要はないものと思いますが、
Deployment構成の検証をおこなっていた際に、ログの出力が増えた時間帯でメモリの使用量が大幅に増加することがありました


メモリ使用量の増加

メモリ使用量についてはIssueも幾つか上がっていますが、Alloyの仕様としてログの転送遅延が生じた際に未転送のログをメモリ内に保持するようになっており、ログの収集量が転送量を上回っている時に増加する傾向にあるようです
メモリが上昇しているタイミングでGrafanaにアクセスしLokiのログを確認すると、平常時は数秒の転送遅延のところ数十分程度の遅れが生じていることもありました

https://github.com/grafana/alloy/issues/3457

解決方法としては、Alloyの台数を増やしたりオートスケーリングを導入して、1台あたりの負荷を低減させる必要があります
公式のHelm Chartではメモリ使用量に応じたHPAによるスケーリングが定義できるようなので、活用するとよさそうです
alloy.resourcesの設定もワークロードへの影響を低減できるでしょう

https://artifacthub.io/packages/helm/grafana/alloy#controller-autoscaling

too_far_behind, greater_than_max_sample_age エラーの発生

環境に適用して観察していると、 loki_discarded_samples_total にて too_far_behindgreater_than_max_sample_age といったreasonのエラーが多数発生することがありました


発生したエラー

Lokiに関するエラーはこちらに詳しくまとめられています
確認すると、ログストリームの送信順序が巻き戻っていたり、現在時刻に対して古すぎるログが送信された際に発生するエラーのようです

https://grafana.com/docs/loki/latest/operations/request-validation-rate-limits/#too_far_behind-and-out_of_order

これは、実装方式の節で説明したログのtailロジックが、Alloyの再起動などで送信をおこなった現在位置を見失ってしまった場合に取得できるログを全てLokiバックエンドに対して送信するために発生するものです
不正なログはLokiバックエンド側のバリデーションによって弾かれるため直接的な問題はありませんが、大量のログを一括転送するため、I/Oやリソース使用量に負荷がかかる可能性があります

エラーの発生を低減させる方法の1つとして、loki.process において stage.drop を設定することができます

https://grafana.com/docs/alloy/latest/reference/components/loki/loki.process/#stagedrop

older_than を短くするほどエラーが減少するはずですが、転送遅延やPodの再起動が生じるケースにおいてログ欠損のリスクが高まります
loki.process コンポーネントでドロップされたログは loki_process_dropped_lines_total にて確認できます

config.alloy
loki.process "pod_logs" {
  // 一定期間より古いログは転送しない
  stage.drop {
    older_than          = "6h"
    drop_counter_reason = "too old"
  }
}

より根本的な解決策として、 loki.source.file および loki.source.kubernetes コンポーネントで tail_from_end を利用することもできます

このオプションを true に設定すると、Alloyの再起動時などにおいてログのtail位置がわからない時に現在時点以降のログのみフィルタして転送するようになるため、エラーの発生やログに関するI/Oが大幅に低減されることが期待できます
一方で、再起動中に発生したログや、Pod内のメモリに保持されていたログは欠損することになるため、ログ連携の要件に応じて設定可否を判断する必要があります

https://grafana.com/docs/alloy/latest/reference/components/loki/loki.source.file/#arguments

https://github.com/grafana/alloy/pull/3883

おわりに

AlloyではNodeのログやKubernetes Eventsも収集できるので、引き続き改善を進めていきたいと思います🐣

https://grafana.com/docs/alloy/latest/reference/components/loki/loki.source.kubernetes_events/

みてねではSREをはじめ、様々なポジションで募集をおこなっています
カジュアル面談からでもOKなので、ご応募お待ちしています!

https://team.mitene.us/

みてね Tech Blog
みてね Tech Blog

Discussion