ログ基盤をPromtailからGrafana Alloyに移行する
家族アルバム みてねでSREをしているyktakaha4と申します🐧
本記事では、みてねのコンテナログ転送基盤をPromtailからGrafana Alloyへ移行した際に検討したポイントを整理します
参考になる部分があれば幸いです
Promtailについて
PromtailはGrafana Loki向けに設計されたログ収集エージェントで、各ノードやコンテナからログを取得し、整形・ラベル付けをおこなってLokiに送信します
KubernetesにおいてはDaemonSetとしてデプロイすることで、クラスタ全体のログを効率的に収集することができます
みてねではインフラとしてAmazon EKSを採用しており、アプリケーションが出力するコンテナログをPromtailで収集してLokiに転送し、Grafanaでログの可視化や検索をおこなっています
オブザーバビリティの全体像については以下の記事に詳しくまとまっていますので、よければご覧ください
便利に利用していたPromtailですが、後述するGrafana Alloyの登場によりGrafana Agentが非推奨化されたことに伴って、2025/02/13にLTSへ移行し、2026/03/02にEOL予定と発表されています
ログ基盤は日々の開発や調査に欠かせないもののため、公式に推奨されているAlloyへの移行を検討し始めました
Grafana Alloyについて
Grafana Alloyは、メトリクス・トレース・ログといった観測データを一元的に収集・転送できるOSSです
従来提供されていたGrafana Agentと比較してOpenTelemetry Collector互換であることが謳われており、Alloyを導入するだけで様々なユースケースに対応できます
Alloyでは処理をComponentという単位で定義します。設定ファイルはHCL形式で記述され、コンポーネント間の依存関係を宣言的に定義できるようになっています
本来、メトリクスやトレースの収集にも活用できますが、前述の通りみてねでは既に複数のツールを組み合わせてオブザーバビリティを実現しているため、今回は廃止されるPromtailの移行に絞って紹介していきます
少し詳細を見てみる
記事執筆にあたって理解を深めるため、AlloyとOpenTelemetryの関係性について少し調べてみました
(Alloy移行にあたって必須の知識ではないので、読み飛ばしても差し支えありません)
弊社で横断SREをしているYoshiiさんがOTel入門にちょうどいい文量のBookを執筆していたのでありがたかったです
なんとなく全体感が掴めたところで、公式が提供しているOTLPの仕様も読んでみます
Promtailから移行する私たちの構成はLegacy First-Party Applications Logsに該当するようです
Alloy(≒OpenTelemetry Collector)ネイティブの構成ではトレース情報などのコンテキストを付与してファイルを介さずログ転送できるようなので、今後検討していきたいところです
https://opentelemetry.io/docs/specs/otel/logs/ より引用
加えてログを表現するデータモデルや、クライアントからログを送信するためのProtocol Buffersが定義されていることも確認しました
Alloyにおいてはotelcol配下にOpenTelemetryに関連するコンポーネントがまとめられており、OTLPからLoki形式へのログ変換はotelcol.exporter.loki、その逆はotelcol.receiver.lokiによっておこなえます
また、前述したgRPCサーバを起動するにはotelcol.receiver.otlpコンポーネントを利用するようです
いずれも今回直接的に利用する機能ではありませんが、AlloyがどのようにOTelとの互換性を実現しているか、なんとなく理解できたように思います
実装方式
話を戻して、移行時に参考にした情報についてご紹介します
公式から提供されている移行方法としてはMigrate from Promtail to Grafana Alloyというドキュメントがありますが、設定ファイルの移行方法の説明が中心でこれだけだと全体感が掴みづらいです
ログ転送に関する設定はCollect Kubernetes logs and forward them to Lokiで詳しく紹介されています
より包括的な構成を理解するにはDeploy Grafana Alloyという節の情報が役立ちます
デプロイ方式について、メリット・デメリットを比較しながら複数紹介されています
公式からHelm Chartも提供されているため、利用するとよさそうです
これらを踏まえて実装をおこなうのですが、デプロイやログ転送方式の組み合わせによって様々な構成を取れるため、少々混乱する場面がありました
本記事では移行の際の叩き台として使えるよういくつかパターンを提示しますので、ご自身のユースケースに併せて検討いただければと思います🐏
loki.source.file
① DaemonSet + Promtailと同様にAlloyをDaemonSetとして配置し、各Nodeのログファイルから収集をおこなう構成です
最終的にみてねで選択した方式になります
Helm Chartで特に必要となる設定を示します
alloy:
# Alloyの設定ファイルを独自に管理
configMap:
create: false
name: alloy-config
key: config.alloy
# AlloyのPodにNodeの /var/log/* をマウント
mounts:
varlog: true
controller:
# DaemonSetとして配置
type: daemonset
apiVersion: v1
kind: ConfigMap
metadata:
name: alloy-config
data:
config.alloy: |
// ここにHCLを書いていく
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されているため、今後独自の実装に移行していく想定なのかもしれません)
loki.source.kubernetes
② Deployment + クラスタリング + AlloyをDeploymentとして配置した上で、Kubernetes API経由で各Podのログを収集する方式です
①との比較を示します
- 👍
/var/log/*
のマウントが不要のため、非rootユーザーで運用できる - 👍 Alloyを実サービスと異なるインフラで運用できる(その際の構成として推奨されている)
- 👎 ログ収集の過程で①の構成では生じないネットワークI/Oが発生する
- 👎 ログの量が時間によって増減する場合、Alloyのオートスケーリングなどの対策をおこなう必要があり実装が複雑化する
Helm Chartの設定は以下です
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
の設定を示します
// 書き込み先の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へ転送する挙動になるため注意しましょう
なお、loki.source.kubernetes におけるログのtailは kubernetes/client-go でおこなわれているようでした
loki.source.kubernetes
③ DaemonSet + 基本的には①または②の構成を選択することが多いのではないかと思うのですが、DaemonSetを使いつつログの取得元はKubernetes APIにするという構成も選択可能です
①の構成をベースに、 local.file_match
と loki.source.file
を削除し loki.source.kubernetes
に置き換えれば実現できます
loki.source.kubernetes
④ Deployment + ログの量が一定、かつ出力量が少なく単一のAlloyで賄える場合には、②のクラスタリング設定を無効化した状態で運用することも可能です
loki.source.kubernetes
のデフォルトの挙動として、Podの起動時(含む再起動)にはKubernetes APIから全期間のログを再びtailして再転送するため、短時間のPod停止であれば欠損なくログを転送することができます
開発環境など、コストを最小限にしたいケースで有用な場合があるかもしれません
⑤ StatefulSet(未検証)
今回は検証しなかったのですが、Alloyをメトリクス収集に用いる場合は、バックエンドの書き込み遅延が発生した時のためにWAL(Write-Ahead Logging)を利用することができます
そのような構成においては、AlloyをStatefulSetで展開することも可能のようです
ログ転送という文脈では、①および②でtailerの実装を確認したようにAlloyはステートレスに構成できるため、状態を考慮した実装は不要です(たぶん)
役立った機能
移行作業中に便利だった機能についてご紹介します
alloy convert
コマンド
実装方式の節でも紹介しましたが、AlloyのCLIには convert というサブコマンドが用意されており、PromtailやPrometheusなどの設定ファイルをAlloyで読み込める形式に変換することができます
Dockerで利用する方法を示します
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 Chartで serviceMonitor.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発生数 |
livedebugging
オプション
Alloy UIとAlloyはデフォルトでデバッグ用のUIを備えており、Podの 12345
ポートにアクセスすると確認できます
アクセスするにはport-forwardコマンドが便利です
kubectl port-forward <alloy-pod-name> 12345:12345
デバッグUIのGraph画面では、Alloyで定義した各Componentの依存関係や、Component間でどの程度のトラフィックが発生しているか視覚的に確認できます
Graph画面
より詳細なログの転送状況を確認したい場合は、livedebuggingオプションを利用できます
設定ファイルで簡単に有効化できます
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のログを転送する際の目安として以下の値が示されています
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のログを確認すると、平常時は数秒の転送遅延のところ数十分程度の遅れが生じていることもありました
解決方法としては、Alloyの台数を増やしたりオートスケーリングを導入して、1台あたりの負荷を低減させる必要があります
公式のHelm Chartではメモリ使用量に応じたHPAによるスケーリングが定義できるようなので、活用するとよさそうです
alloy.resources
の設定もワークロードへの影響を低減できるでしょう
too_far_behind
, greater_than_max_sample_age
エラーの発生
環境に適用して観察していると、 loki_discarded_samples_total
にて too_far_behind
や greater_than_max_sample_age
といったreasonのエラーが多数発生することがありました
発生したエラー
Lokiに関するエラーはこちらに詳しくまとめられています
確認すると、ログストリームの送信順序が巻き戻っていたり、現在時刻に対して古すぎるログが送信された際に発生するエラーのようです
これは、実装方式の節で説明したログのtailロジックが、Alloyの再起動などで送信をおこなった現在位置を見失ってしまった場合に取得できるログを全てLokiバックエンドに対して送信するために発生するものです
不正なログはLokiバックエンド側のバリデーションによって弾かれるため直接的な問題はありませんが、大量のログを一括転送するため、I/Oやリソース使用量に負荷がかかる可能性があります
エラーの発生を低減させる方法の1つとして、loki.process において stage.drop
を設定することができます
older_than
を短くするほどエラーが減少するはずですが、転送遅延やPodの再起動が生じるケースにおいてログ欠損のリスクが高まります
loki.process
コンポーネントでドロップされたログは loki_process_dropped_lines_total
にて確認できます
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内のメモリに保持されていたログは欠損することになるため、ログ連携の要件に応じて設定可否を判断する必要があります
おわりに
AlloyではNodeのログやKubernetes Eventsも収集できるので、引き続き改善を進めていきたいと思います🐣
みてねではSREをはじめ、様々なポジションで募集をおこなっています
カジュアル面談からでもOKなので、ご応募お待ちしています!
Discussion