Kubernetes cluster と Grafana Cloud を Alloy で連携してみる(後編) 〜Getting Started Guide の先へ〜
前回は Grafana Cloud の Getting Started Guide に従って Alloy をインストールする方法や、Kubernetes Monitoring Helm chart の values の読み解き方や、secret を別で管理する方法について説明しました。
今回は設定の変更方法、デバッグ方法、OpenTelemetry との連携方法に焦点を当てます。
前回同様、Kubernetes Monitoring Helm chart で Alloy がインストールされていて、namespace は alloy、release name は grafana-k8s-monitoring であるものとします。
config.alloy の変更方法
Kubernetes Monitoring Helm chart では values を変更することで各種 Alloy が利用する設定 (config.alloy) の内容を変更することができます。
例えば、次のように Windows exporter を無効化してみます。
clusterMetrics:
enabled: true
kube-state-metrics:
podAnnotations:
kubernetes.azure.com/set-kube-service-host-fqdn: "true"
windows-exporter:
deploy: false
enabled: false
すると、grafana-k8s-monitoring-alloy-metrics の config.alloy から Windows exporter に関する設定が消えます。
@@ -540,64 +501,6 @@
}
forward_to = argument.metrics_destinations.value
}
-
- // Windows Exporter
- discovery.kubernetes "windows_exporter_pods" {
- role = "pod"
- selectors {
- role = "pod"
- label = "app.kubernetes.io/name=windows-exporter,release=grafana-k8s-monitoring"
- }
- namespaces {
- names = ["alloy"]
- }
- }
-
- discovery.relabel "windows_exporter" {
- targets = discovery.kubernetes.windows_exporter_pods.targets
-
- // keep only the specified metrics port name, and pods that are Running and ready
- rule {
- source_labels = [
- "__meta_kubernetes_pod_container_port_name",
- "__meta_kubernetes_pod_container_init",
- "__meta_kubernetes_pod_phase",
- "__meta_kubernetes_pod_ready",
- ]
- separator = "@"
- regex = "metrics@false@Running@true"
- action = "keep"
- }
-
- // Set the instance label to the node name
- rule {
- source_labels = ["__meta_kubernetes_pod_node_name"]
- target_label = "instance"
- }
- }
-
- prometheus.scrape "windows_exporter" {
- targets = discovery.relabel.windows_exporter.output
- job_name = "integrations/windows-exporter"
- scrape_interval = "60s"
- scrape_timeout = "10s"
- scrape_protocols = ["OpenMetricsText1.0.0","OpenMetricsText0.0.1","PrometheusText0.0.4"]
- scrape_classic_histograms = false
- clustering {
- enabled = true
- }
- forward_to = [prometheus.relabel.windows_exporter.receiver]
- }
-
- prometheus.relabel "windows_exporter" {
- max_cache_size = 100000
- rule {
- source_labels = ["__name__"]
- regex = "up|scrape_samples_scraped|windows_.*|node_cpu_seconds_total|node_filesystem_size_bytes|node_filesystem_avail_bytes|container_cpu_usage_seconds_total"
- action = "keep"
- }
- forward_to = argument.metrics_destinations.value
- }
}
cluster_metrics "feature" {
metrics_destinations = [
@@ -1220,7 +1123,7 @@
# HELP grafana_kubernetes_monitoring_feature_info A metric to report the enabled features of the Kubernetes Monitoring Helm chart
# TYPE grafana_kubernetes_monitoring_feature_info gauge
grafana_kubernetes_monitoring_feature_info{feature="applicationObservability", protocols="otlpgrpc,otlphttp,zipkin", version="1.0.0"} 1
- grafana_kubernetes_monitoring_feature_info{deployments="kube-state-metrics,node-exporter,windows-exporter", feature="clusterMetrics", sources="kubelet,kubeletResource,cadvisor,kube-state-metrics,node-exporter,windows-exporter", version="1.0.0"} 1
+ grafana_kubernetes_monitoring_feature_info{deployments="kube-state-metrics,node-exporter", feature="clusterMetrics", sources="kubelet,kubeletResource,cadvisor,kube-state-metrics,node-exporter", version="1.0.0"} 1
grafana_kubernetes_monitoring_feature_info{feature="clusterEvents", version="1.0.0"} 1
grafana_kubernetes_monitoring_feature_info{feature="podLogs", method="volumes", version="1.0.0"} 1
grafana_kubernetes_monitoring_feature_info{feature="integrations", sources="alloy", version="1.0.0"} 1
また、Extra Configuration や Extra Rules で紹介されているように、任意の設定を追加することも可能です。
例えば、次のように podLogs.extraLogProcessingStages
と alloy-logs.extraConfig
を指定してみます。
podLogs:
enabled: true
extraLogProcessingStages: |
// This is a comment in loki.process.pod_logs
alloy-logs:
enabled: true
extraConfig: |
// This is an extra config
すると、grafana-k8s-monitoring-alloy-logs の config.alloy の内容が次のように変わります。
@@ -1432,6 +1432,7 @@
"service_instance_id" = "service_instance_id",
}
}
+ // This is a comment in loki.process.pod_logs
// Only keep the labels that are defined in the `keepLabels` list.
stage.label_keep {
@@ -1449,7 +1450,7 @@
-
+ // This is an extra config
// Destination: grafana-cloud-logs (loki)
otelcol.exporter.loki "grafana_cloud_logs" {
forward_to = [loki.write.grafana_cloud_logs.receiver]
もし直接 config.alloy を記述したい場合、alloy-*.alloy.configMap.content
に指定することも可能です。
alloy-logs:
enabled: true
alloy:
configMap:
create: true
name: grafana-k8s-monitoring-alloy-logs-custom
content: |
// Feature: Pod Logs
declare "pod_logs" {
argument "logs_destinations" {
comment = "Must be a list of log destinations where collected logs should be forwarded to"
}
// -- snip --
この場合、name には Helm chart で作られる ConfigMap とは異なるものを指定してください。上記の例では Helm chart で作成される ConfigMap の名前に -custom
suffix を付与しています。
もし同じ名前を指定した場合、grafana-k8s-monitoring-alloy-operator で次のようなエラーが出て ConfigMap の内容の更新に失敗します。
$ kubectl logs -n alloy deploy/grafana-k8s-monitoring-alloy-operator
-- snip --
{"level":"error","ts":"2025-09-26T11:30:32Z","logger":"helm.controller","msg":"Failed to sync release","namespace":"alloy","name":"grafana-k8s-monitoring-alloy-logs","apiVersion":"collectors.grafana.com/v1alpha1","kind":"Alloy","release":"grafana-k8s-monitoring-alloy-logs","error":"failed to get candidate release: Unable to continue with update: ConfigMap \"grafana-k8s-monitoring-alloy-logs\" in namespace \"alloy\" exists and cannot be imported into the current release: invalid ownership metadata; annotation validation error: key \"meta.helm.sh/release-name\" must equal \"grafana-k8s-monitoring-alloy-logs\": current value is \"grafana-k8s-monitoring\"","stacktrace":"github.com/operator-framework/operator-sdk/internal/helm/controller.HelmOperatorReconciler.Reconcile\n\t/workspace/internal/helm/controller/reconcile.go:224\nsigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).Reconcile\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:119\nsigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).reconcileHandler\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:340\nsigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).processNextWorkItem\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:300\nsigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).Start.func2.1\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:202"}
{"level":"error","ts":"2025-09-26T11:30:32Z","msg":"Reconciler error","controller":"alloy-controller","object":{"name":"grafana-k8s-monitoring-alloy-logs","namespace":"alloy"},"namespace":"alloy","name":"grafana-k8s-monitoring-alloy-logs","reconcileID":"2e4d2421-24ff-4346-a866-ce37e6c13dab","error":"failed to get candidate release: Unable to continue with update: ConfigMap \"grafana-k8s-monitoring-alloy-logs\" in namespace \"alloy\" exists and cannot be imported into the current release: invalid ownership metadata; annotation validation error: key \"meta.helm.sh/release-name\" must equal \"grafana-k8s-monitoring-alloy-logs\": current value is \"grafana-k8s-monitoring\"","stacktrace":"sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).reconcileHandler\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:353\nsigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).processNextWorkItem\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:300\nsigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).Start.func2.1\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.21.0/pkg/internal/controller/controller.go:202"}
これは、Alloy custom resource を処理する alloy-operator の内部で Alloy Helm chart が利用されており、configMap.create
が true の場合は Alloy Helm chart によって ConfigMap を作成しようとするからです。もし Kubernetes Monitoring Helm chart で作成される ConfigMap と同じ名前だと、他の Helm chart で作成された ConfigMap を Alloy Helm chart が更新しようとするため、エラーになってしまうのだと思われます。
config.alloy の反映方法
Config Reloader で軽く言及されていますが、Helm chart で Alloy をインストールすると、デフォルトで config reloader が sidecar container として定義されます。config reloader は設定の変更を検知すると http://localhost:12345/-/reload
に POST リクエストを送るので、設定を反映するために Pod を作り直す必要はありません。むしろ、Kustomize considerations で言及されているように、起動に時間がかかる可能性があるため、設定ファイルの変更を反映するために Pod を作り直すのは避けるべきとされています。
ただし、Mounted ConfigMaps are updated automatically で説明されているように、ConfigMap の更新が Pod に反映されるまでにそれなりのタイムラグがあるので、開発時に即時反映させたい場合は kubectl rollout restart
や kubectl delete pod
で Pod を再作成するのが良いでしょう。
Pod を再作成しない場合、Alloy collector のログを確認することで設定がリロードされたかどうか判断することができます。設定がリロードされた場合、以下のように “config reloaded” というログが出ます。
$ kubectl -n alloy logs ds/grafana-k8s-monitoring-alloy-logs -f
-- snip --
ts=2025-10-01T17:39:40.345817877Z level=info msg="reload requested via /-/reload endpoint" service=http
-- snip --
ts=2025-10-01T17:39:40.351081419Z level=info msg="config reloaded" service=http
Alloy のデバッグ方法
Debug Grafana Alloy で紹介されているように、Alloy は Web UI を提供しています。
Kubernetes Monitoring Helm chart でインストールすると、各 Alloy collector に対応する Service も作成されるので、Service に対して port forwarding して Web UI にアクセスすると良いでしょう。例えば grafana-k8s-monitoring-alloy-logs の Web UI にアクセスしたい場合は次のコマンドを実行します。
kubectl port-forward -n alloy service/grafana-k8s-monitoring-alloy-logs 12345:12345
port forwarding した上で http://localhost:12345/graph にアクセスすると各コンポーネントの依存関係が可視化されて理解の助けになります。
また、先ほど Alloy collector のログを確認することで設定がリロードされたか判断する方法を紹介しましたが、Alloy の Web UI にアクセスして設定情報を確認するのも良いでしょう。例えば grafana-k8s-monitoring-alloy-logs の loki.process.pod_logs
の内容を更新した場合、http://localhost:12345/component/pod_logs.feature/loki.process.pod_logs にアクセスすれば現在利用されている設定情報を確認できます。2 分経っても更新されない場合、config.alloy の内容に問題があって反映されていない可能性があるので Alloy collector のログを確認すると良いでしょう。
更に高度なデバッグ方法として delve を使ったデバッグが考えられますが、この方法については詳説 Alloy loki.process で紹介しているのでそちらを参照してください。
OpenTelemetry 連携: Pod のログと OTLP receiver のログの labels を統一する
OpenTelemetry Logging の冒頭でも言及されていますが、ログはメトリクスやトレースデータの収集と違い、既存のシステムがあるので厄介です。
Kubernetes Monitoring Helm chart で Alloy をインストールすると、Pod が stdout/stderr に出力したログは grafana-k8s-monitoring-alloy-logs が収集しますが、OTLP exporter から送信されたログは grafana-k8s-monitoring-alloy-receiver が処理します。OTLP の gRPC エンドポイントは http://grafana-k8s-monitoring-alloy-receiver.alloy.svc.cluster.local:4317 です。
OpenTelemetry としては、New First-Party Application Logs にあるように、これから自分たちで開発するアプリケーションに関しては OTLP でログを送ることを推奨しています。
とはいえ、アプリケーション側で OTLP を使う方針にしたとしても、アプリケーションで利用しているライブラリが stdout にログを出力することもあれば、例外発生時のログが stderr に出力されることもあるので、同じ Pod のログでも OTLP のログと stdout/stderr のログが混ざってしまいます。
Loki 上でログを確認する上で、OTLP のログと stdout/stderr のログで labels や structured metadata が異なっていると、ログを検索する際のフィルタリング条件によっては片方のログしかヒットしないことがあるので困りものです。
ところが、デフォルトの設定だと以下のように labels、structured metadata にけっこうな違いがあります。service_name
も値が異なります。
stdout/stderr | OTLP | |
---|---|---|
labels | cluster container job k8s_cluster_name namespace service_name service_namespace |
k8s_cluster_name k8s_namespace_name k8s_pod_name service_name |
structured metadata | detected_level pod service_instance_id |
cluster detected_level flags host_name k8s_node_name k8s_pod_ip loki_resource_labels namespace observed_timestamp os_type pod scope_name severity_number severity_text span_id telemetry_sdk_language telemetry_sdk_name telemetry_sdk_version trace_id |
では、OTLP を使わずに stdout/stderr に寄せれば良いかというと、詳説 Alloy loki.process の「JSON のログをパースして変換する」で説明しているように、アプリケーション側で付与した attribute を structured meatdata として処理するのは難しそうです。
よって、stdout/stderr のログと OTLP のログの labels と structured metadata を極力揃える必要があるわけですが、それには Pod の定義と config.alloy の両方で工夫する必要があります。
重要な label は Pod にアノテーションと環境変数を付与する
grafana-k8s-monitoring-alloy-logs では discovery.kubernetes.pods
で検出された Pod に対して discovery.relabel.filtered_pods
で relabel します。
discovery.kubernetes "pods" {
role = "pod"
selectors {
role = "pod"
field = "spec.nodeName=" + sys.env("HOSTNAME")
}
}
discovery.relabel "filtered_pods" {
targets = discovery.kubernetes.pods.targets
rule {
source_labels = ["__meta_kubernetes_namespace"]
action = "replace"
target_label = "namespace"
}
rule {
source_labels = ["__meta_kubernetes_pod_name"]
action = "replace"
target_label = "pod"
}
// -- snip --
}
relabel の rule 中には次のように How service.name should be calculated に従って処理する rule も存在します。
// explicitly set service_name. if not set, loki will automatically try to populate a default.
// see https://grafana.com/docs/loki/latest/get-started/labels/#default-labels-for-all-users
//
// choose the first value found from the following ordered list:
// - pod.annotation[resource.opentelemetry.io/service.name]
// - pod.label[app.kubernetes.io/name]
// - k8s.pod.name
// - k8s.container.name
rule {
action = "replace"
source_labels = [
"__meta_kubernetes_pod_annotation_resource_opentelemetry_io_service_name",
"__meta_kubernetes_pod_label_app_kubernetes_io_name",
"__meta_kubernetes_pod_container_name",
]
separator = ";"
regex = "^(?:;*)?([^;]+).*$"
replacement = "$1"
target_label = "service_name"
}
そのため、Pod に resource.opentelemetry.io/service.name
annotation を付与することで、stdout/stderr のログに任意の service_name
label を付与することができます。
OTLP のログに関しては、OTEL_RESOURCE_ATTRIBUTES
環境変数で service.name
を指定することで任意の service_name
label を付与することができます。
よって、Pod に resource.opentelemetry.io/service.name
annotation と OTEL_RESOURCE_ATTRIBUTES
環境変数の両方を指定することで、それぞれのログの service_name
label を統一することができます。
例えば、次の manifest では service_name
を hello、deployment_environment_name
を staging に統一するために annotation と OTEL_RESOURCE_ATTRIBUTES
環境変数の両方を指定しています。
apiVersion: v1
kind: Pod
metadata:
generateName: hello-otel-
annotations:
resource.opentelemetry.io/service.name: hello
resource.opentelemetry.io/deployment.environment.name: staging
spec:
containers:
- name: hello-otel
image: ghcr.io/abicky/opentelemetry-collector-k8s-example/hello-otel:latest
env:
- name: OTEL_RESOURCE_ATTRIBUTES
value: service.name=hello,deployment.environment.name=staging
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://grafana-k8s-monitoring-alloy-receiver.alloy.svc.cluster.local:4317
restartPolicy: Never
なお、次のように otelcol.processor.k8sattributes
の設定を変更すれば OTEL_RESOURCE_ATTRIBUTES
が不要になると思われるかもしれませんが、OpenTelemetry Collector の OTLP receiver で受け取ったデータに Kubernetes attributes processor で attribute を付与するで言及しているように、Pod 起動直後のログには attribute が付与されないので、確実に付与したい label は OTEL_RESOURCE_ATTRIBUTES
で指定した方が良いでしょう。
applicationObservability:
enabled: true
processors:
k8sattributes:
annotations:
- from: pod
key_regex: ^resource\.opentelemetry\.io/(.+)$
tag_name: $$1
同様の理由から、OTLP のログの labels、structured metadata のうち、otelcol.processor.k8sattributes が付与している次のものに関しては起動直後のログには付与されない可能性があります。
- k8s_namespace_name
- k8s_pod_name
- k8s_node_name
もし確実に付与したいなら、これらの情報についても次のように OTEL_RESOURCE_ATTRIBUTES
に指定すると良いでしょう。
apiVersion: v1
kind: Pod
metadata:
generateName: hello-otel-
annotations:
resource.opentelemetry.io/service.name: hello
resource.opentelemetry.io/deployment.environment.name: staging
spec:
containers:
- name: hello-otel
image: ghcr.io/abicky/opentelemetry-collector-k8s-example/hello-otel:latest
env:
- name: K8S_NAMESPACE_NAME
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: K8S_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: K8S_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: OTEL_RESOURCE_ATTRIBUTES
value: service.name=hello,deployment.environment.name=staging,k8s.namespace.name=$(K8S_NAMESPACE_NAME),k8s.pod.name=$(K8S_POD_NAME),k8s.node.name=$(K8S_NODE_NAME)
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://grafana-k8s-monitoring-alloy-receiver.alloy.svc.cluster.local:4317
restartPolicy: Never
logs と receiver の config.alloy を変更する
OTLP のログに label を付与するのはなかなか厄介です。というのも、Loki の設定で OpenTelemetry のどの attribute を label とみなすか設定されているようなんですが、Grafana Cloud では現状その設定をカスタマイズできないからです。label とみなされない attribute は全て structured metadata になります。
なので、stdout/stderr のログと OTLP のログで labels と structured metadata を揃えようと思うと、stdout/stderr のログで label になっていて Loki が label として処理する OpenTelemetry の attribute は付与しつつ、それが難しくてあまり重要そうでない stdout/stderr の label は structured metadata に変更する方針を取ることになります。
まず、stdout/stderr のログの namespace
を label ではなく structured metadata にするには、次のように podLogs.structuredMetadata
に namespace
を追加します。
podLogs:
enabled: true
structuredMetadata:
namespace: namespace
stdout/stderr のログには k8s_node_name
structured metadata が付与されていませんが、便利そうなので付与します。
podLogs:
enabled: true
extraDiscoveryRules: |
rule {
source_labels = ["__meta_kubernetes_pod_node_name"]
action = "replace"
target_label = "k8s_node_name"
}
structuredMetadata:
k8s.node.name: k8s.node.name
namespace: namespace
また、Default labels for OpenTelemetry の CAUTION にあるように、k8s.pod.name
をインデキシングするのは非推奨で、grafana-k8s-monitoring-alloy-receiver の設定では k8s.pod.name
の値が pod
label にも付与される設定になっているので、冗長な k8s.pod.name
は削除するのが良いでしょう。loki.resource.labels
も otelcol.exporter.loki のためのもので、デフォルトである otelcol.exporter.otlphttp を使う分には不要なはずなので削除します。
destinations:
- name: gc-otlp-endpoint
type: otlp
processors:
resourceAttributes:
removeList:
- k8s.pod.name
- loki.resource.labels
stdout/stderr のログには service_namespace
label があって、OTLP のログには k8s_namespace_name
label があるので、service_namespace
に統一します。
applicationObservability:
enabled: true
logs:
transforms:
resource:
- set(attributes["service.namespace"], attributes["k8s.namespace.name"]) where attributes["service.namespace"] == nil
- delete_key(attributes, "k8s.namespace.name") where attributes["k8s.namespace.name"]) == attributes["service.namespace"]
ここまですると、labels と structured metadata の差異は次のようにだいぶ解消します。
stdout/stderr | OTLP | |
---|---|---|
labels | container job k8s_cluster_name service_name service_namespace |
k8s_cluster_name service_name service_namespace |
structured metadata | detected_level k8s_node_name namespace pod service_instance_id |
cluster detected_level flags host_name k8s_node_name k8s_pod_ip namespace observed_timestamp os_type pod scope_name severity_number severity_text span_id telemetry_sdk_language telemetry_sdk_name telemetry_sdk_version trace_id |
関連する values のみまとめると以下のとおりです。
destinations:
- name: gc-otlp-endpoint
processors:
resourceAttributes:
removeList:
- k8s.pod.name
- loki.resource.labels
podLogs:
enabled: true
structuredMetadata:
service.instance.id: service.instance.id
namespace: namespace
extraDiscoveryRules: |
rule {
source_labels = ["__meta_kubernetes_pod_node_name"]
action = "replace"
target_label = "k8s_node_name"
}
applicationObservability:
enabled: true
logs:
transforms:
resource:
- set(attributes["service.namespace"], attributes["k8s.namespace.name"]) where attributes["service.namespace"] == nil
- delete_key(attributes, "k8s.namespace.name") where attributes["k8s.namespace.name"] == attributes["service.namespace"]
Helm chart の values を理解するで説明したとおり、どの values を変更すれば所望の設定になるか特定するのはなかなか大変です。参考までに各機能のドキュメントのリンクを載せておきます。
おわりに
以上、前編、後編の 2 回に分けて、Kubernetes cluster と Grafana Cloud を Alloy で連携する際の知見について説明してきました。これらの知見がこれから Grafana Cloud を利用する方の参考になれば幸いです。