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 ConfigurationExtra Rules で紹介されているように、任意の設定を追加することも可能です。
例えば、次のように podLogs.extraLogProcessingStagesalloy-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 restartkubectl 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 になります。

cf. [Grafana Cloud OTLP Endpoint] Support specifying OTel Resource Attributes promoted as Loki labels #13044

なので、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.structuredMetadatanamespace を追加します。

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.labelsotelcol.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 を利用する方の参考になれば幸いです。

広告
詳説 Alloy loki.process