
Kubernetes
イベント
該当するコンテンツが見つかりませんでした
マガジン
技術ブログ
はじめに CyberAgent group Infrastructure Unit(CIU)の川原で ...
1. はじめに こんにちは。プラットフォームエンジニアリングチームに所属する小泉( @naotoko_ )です。 本記事は、同チームの徳富( @yannKazu1 )が執筆した「 消えるランナーの観測基盤をどう選んだか — Datadog・マネージド・OSS を料金体系で比べて Loki + Prometheus に決めた話 」の本番環境への導入編です。EKS Auto Mode でホストしている Self-hosted Runner の監視基盤に Grafana・Prometheus・Loki・Alloy を導入した際にハマったポイントと解決策を、各コンポーネントごとにお伝えします。 Grafana・Prometheus・Loki・Alloy をどのような構成で実装したかは、上記ブログの「 実装:どう組んだか 」をご覧ください。 2. Alloy - メトリクスが重複する・取れない 2-1. DaemonSet の重複 scrape 問題 Alloy は DaemonSet でデプロイしており、全ノードに1つずつ Pod が立ち上がる構成となっています。 デフォルトのまま使用すると、Alloy はメトリクス収集時にクラスター全体から scrape 対象の一覧を取得し、そのリストに対して定期的に HTTP リクエストを送ります。DaemonSet の各 Pod がそれぞれ独立してこれを実行するため、全 Pod が同じターゲット一覧を取得してしまいます。 たとえば kube-state-metrics や ARC controller のようにクラスターに1つしかないエンドポイントは、全ての Alloy Pod が同じエンドポイントを scrape しに行くためノード数分重複します。さらに、node-exporter や kubelet のようにノードごとに存在するエンドポイントでも、各 Alloy Pod がクラスター全ノード分のエンドポイントを発見して全て scrape するため、同様にノード数分重複します。その結果、全ての scrape 対象で同一のメトリクスが重複して Prometheus に送られてしまいます。 各 Alloy Pod がそれぞれ独立にクラスター全体のターゲット一覧を取得するため、クラスターに1つしかない kube-state-metrics も、ノードごとに存在する kubelet も、全 Pod から scrape される。 なぜログ収集では同じ問題が起きないか ログ収集は各 Alloy Pod がノードのローカルファイル( /var/log/pods/ )を読む方式です。ノードAの Alloy はノードAのログだけ、ノードBの Alloy はノードBのログだけを読むため、Pod 間でデータが重複しません。読む対象がノード単位で自然に分割されているため、メトリクスのような重複の問題が起きませんでした。 解決策:Alloy のクラスタリングを有効化する Alloy にはクラスタリング機能があり、Pod 同士がクラスターを形成して scrape 対象を自動的に分担します。(詳しい仕組みは割愛) Alloy Pod 同士がクラスターを組み、各ターゲットの担当をいずれか1つの Pod に決める。担当はハッシュで決まるため、図のように自分と同じノード上のターゲットを担当するとは限らない。 設定は2段階必要です。まず Helm chart の values でクラスタリングを有効化し、DaemonSet の全 Pod がクラスターを形成するようにします。 alloy : clustering : enabled : true そのうえで、分担させたい各 prometheus.scrape ブロックに clustering { enabled = true } を付けます。 prometheus.scrape "kube_state_metrics" { targets = [...] clustering { enabled = true } forward_to = [prometheus.remote_write.default.receiver] } 注意点として、この2つはセットで初めて機能します。Helm values 側を有効化せずに prometheus.scrape 側だけ書いても no-op(何もしない)になり、逆に Helm values 側だけ有効化しても clustering ブロックを付けていない scrape コンポーネントは従来どおり全 Pod が独立に scrape し続けます。 2-2. kubelet scrape の InternalIP 対応 Grafana でメトリクスを確認すると Pod の CPU・メモリが No data になっていました。調べると kubelet の scrape が全ノードで失敗していました。 原因は Alloy が __meta_kubernetes_node_name (ノード名)を scrape 先のアドレスとして使っていたためです。EKS Auto Mode ではノード名がインスタンス ID になるため名前解決できず、全ノードでタイムアウトしてしまっていました。 通常の EKS ではノード名は ip-xxx-xxx-xxx-xxx.ap-northeast-1.compute.internal のようなプライベート DNS 名になるため、 __meta_kubernetes_node_name でも名前解決できます。一方 EKS Auto Mode では i-xxxxxxxxxxxxxxxxx のような EC2 インスタンス ID がノード名になるため、名前解決できないという状況でした。 解決策:InternalIP を使う __meta_kubernetes_node_address_InternalIP に切り替えることで、ノード名ではなく IP アドレスで直接アクセスするようにしました。 rule { source_labels = ["__meta_kubernetes_node_address_InternalIP"] regex = "(.+)" replacement = "${1}:10250" target_label = "__address__" } 3. Prometheus - OOM と欠損との戦い 3-1. OOM 問題 導入後、Prometheus サーバーが OOMKilled → 再起動を繰り返すようになりました。原因は1つではなく、複数が重なっていました。 原因1:Alloy と Prometheus の二重取り込み Prometheus の Helm chart はデフォルトでいくつかの scrape job( kubernetes-nodes-cadvisor ・ kubernetes-pods 等)が有効になっています。今回の構成では Alloy が一元的に scrape して Prometheus に remote_write する設計のため、Prometheus 自身の scrape job と Alloy の remote_write で同じメトリクスが2経路で入っていました。 これは Alloy が scrape するターゲットと重複している chart デフォルトの scrape job を無効化することで解決することができます。 scrapeConfigs : kubernetes-nodes : false kubernetes-nodes-cadvisor : false kubernetes-pods : false # ... 原因2:cAdvisor の高 cardinality ラベル cAdvisor は kubelet に組み込まれており、コンテナのリソース使用量を収集するコンポーネントです。Alloy は kubelet の /metrics/cadvisor エンドポイントを scrape することでこのメトリクスを取得しています。cAdvisor のメトリクスには image ・ name などのラベルが付いており、同時稼働するランナー数が多いほど cardinality(系列数)が爆発的に増えてしまいます。 そこで、Prometheus に送る前の段階で Alloy 側でメトリクスやログ調査に不要なラベルを drop することで、Prometheus に送られる系列数を極力減らすようにしました。 ちなみに、 image はコンテナイメージ名、 name はコンテナランタイム上のコンテナ ID(containerd では 64 桁の 16 進文字列)が入ります。 name はコンテナが起動するたびに異なる値になるためほぼユニークであり、集計や絞り込みには使わないと判断し、この2つのラベルを drop しました。一方で、メトリクスのクエリで実際に使う namespace ・ pod ・ container といったラベルは残しています。環境に応じて、調査に使わないラベルは可能な範囲で drop するようにするのが無難です。 rule { regex = "image|name" action = "labeldrop" } ただし、 id ラベルだけは残す必要があります。cAdvisor は同じメトリクス名で複数のコンテナの情報を収集しており、それぞれを区別するために id ラベルが使われています。 id を drop すると異なるコンテナのメトリクスがラベルセット上で同一系列として扱われていしまいます。その結果、同じタイムスタンプに複数のサンプルが届き、 Prometheus が duplicate sample for timestamp エラーを返してしまうため注意が必要です。 原因3:Head Block のメモリ常駐 Prometheus はメトリクスを受け取ると、書き込み効率のためにまずメモリ上に一時的に貯めます。これが Head Block です。 一定時間が経つとメモリから EBS 上のファイルに書き出されます。デフォルトでは Head Block が3時間分(min-block-duration の1.5倍)に達した時点で古い2時間分がブロックとして書き出されます。つまり常に1〜3時間分のデータがメモリに残り続けるため、メトリクスの量が多いほどメモリ使用量が増えます。 storage.tsdb.min-block-duration を 2h から 30m に短縮することでこの「一時的に貯める時間」を短くし、より頻繁に EBS に書き出すことでメモリ常駐量を削減しました。その代わり、以前はメモリから取得できていたデータが EBS から取得されるためクエリが遅くなるトレードオフがあります。Self-hosted Runner の監視という性質上、確認したいのは基本的に直近の状況であるため、30分より古いデータの取得が多少遅くなっても問題ないと判断しています。 なお、 storage.tsdb.min-block-duration は --help にも表示されない hidden フラグで、公式には「テスト用途」とされています。挙動を理解したうえで利用してください。 extraArgs : storage.tsdb.min-block-duration : 30m 3-2. out-of-order サンプル問題 Alloy クラスタリングを有効化してから、Prometheus に out of order sample エラーが大量に出るようになりました。 Alloy のクラスタリングは Pod の増減をトリガーにターゲットの再配分を行います。Self-hosted Runner はジョブの増減に応じてノードが頻繁にスケールするため、Alloy の Pod も増減し、再配分が頻繁に起きます。Alloy のクラスタリングによるターゲットの分担は eventually consistent なモデルであるため、再配分の引き継ぎのタイミングによっては同じターゲットが一時的に2つの Pod から scrape されることがあります(grafana/alloy の issue #1611・#2348 でも報告されている既知の挙動です)。この状態で同じ時系列のサンプルが2つ届くと、後着のサンプルのタイムスタンプが先着より古い場合があります。すると Prometheus はこれを out-of-order として拒否し、該当のサンプルを捨ててしまいます。その結果、 Grafana で確認できるメトリクスに欠損が生じてしまいます。 解決策:out-of-order time window を設定する out_of_order_time_window を設定することで、指定した時間内の過去のタイムスタンプを受け入れるようになります。弊社の環境では、最終的に 10m に落ち着きました。out-of-order のサンプルを保持するためのメモリが若干増えますが、 storage.tsdb.min-block-duration を 30m に短縮して Head Block のメモリを節約できているため、特に問題ないと判断し、10m に設定しています。 tsdb : out_of_order_time_window : 10m 4. Loki - chart 移管の罠と起動しない Pod 4-1. Helm chart リポジトリ移管の罠 Loki の Helm chart はもともと grafana/helm-charts ( https://grafana.github.io/helm-charts )で配布されていました。 しかし chart v6.55.0 を最後に OSS Loki 向けの chart は grafana-community/helm-charts ( https://grafana-community.github.io/helm-charts )へフォークされ、v7.0.0 以降は community 側からリリースされています。従来のリポジトリに残った loki chart は Grafana Enterprise Logs(GEL)向けのメンテナンス専用になりました。 そのため、 grafana/helm-charts を使い続けると、気づかないうちに GEL 向けの chart を引いてしまいます。 合わせて以下の変更もあるので、注意が必要です。 フォーク後はメジャーバージョンの上がるペースが非常に速い(v7.0.0 から数ヶ月で v17.x、執筆時点の最新は v18.x) deploymentMode の値が SingleBinary → Monolithic にリネーム(chart v12.0.0)。なお values のキーは singleBinary のまま変わっていません resource "helm_release" "loki" { repository = "<https://grafana-community.github.io/helm-charts>" # ここが変わった chart = "loki" version = "17.1.6" ... } # chart v12.0.0 以降 deploymentMode : Monolithic # SingleBinary から変更 4-2. StorageClass が自動作成されない Loki の Pod がスケジュールされず、以下のエラーが出ていました。 eks-auto-mode/compute Failed to schedule pod, unbound pvc must define a storage class 通常の EKS では EBS CSI driver addon がデフォルトの StorageClass を自動作成しますが、EKS Auto Mode では作成されません。StorageClass が作成されていないと、PVC がバインドできず Pod が起動しません。 そのため、StorageClass を手動で作成し、Loki の PVC に指定することで解決しました。 # StorageClass の手動作成 apiVersion : storage.k8s.io/v1 kind : StorageClass metadata : name : auto-ebs-sc provisioner : ebs.csi.eks.amazonaws.com volumeBindingMode : WaitForFirstConsumer parameters : type : gp3 encrypted : "true" # loki.yaml singleBinary : persistence : storageClass : auto-ebs-sc 5. Grafana - デプロイ戦略とコード管理 5-1. EBS(RWO)と RollingUpdate の相性問題 Grafana を helm upgrade したとき、Pod が新しくなるはずが延々と Pending のまま膠着しました。原因は EBS の制約です。 EBS は RWO(ReadWriteOnce)のため、1つの Node にしか同時にアタッチできません。デフォルトの RollingUpdate は新しい Pod を起動してから古い Pod を落とす順番なので、新旧の Pod が同じ PVC を取り合って Multi-Attach error が発生します。 解決策:Recreate 戦略に変更する Recreate にすると「旧 Pod 終了 → EBS detach → 新 Pod 起動」という順番になります。replicas=1 の構成なので更新時に短時間のダウンタイムが発生しますが、監視基盤という特性を踏まえて瞬断は許容し、 Recreate に設定しています。 deploymentStrategy : type : Recreate 5-2. Dashboard・Alerting のコード管理 なぜ Terraform provider ではなく Helm values で管理するか Grafana のダッシュボードとアラートルールをコード管理する方法として、 grafana Terraform provider を使う方法もあります。しかし今回は Helm values(YAML/JSON)での管理を選びました。 理由はシンプルで、Grafana の UI でダッシュボードやアラートルールを作り込んだあと、そのまま JSON/YAML でエクスポートして Helm values に貼り付けるだけでコード管理できるからです。Terraform のリソース定義に落とし込む手間が不要で、UI で確認しながら作ったものをそのまま反映できます。 Grafana の Helm chart は provisioning の仕組みを持っており、values に書いたダッシュボード定義やアラートルールを起動時に自動で読み込みます。 YAML 管理による制約:削除は deleteRules に明示が必要 アラートルールを YAML から削除しても、Grafana 上のルールは消えません。アラートルールの provisioning は追加・更新してくれますが、削除は行いません。 これは Grafana の設計上の安全策です。file-based provisioning はステートレスで、Grafana は「この YAML がルールの全量である」という保証を持てません。複数ファイルから同時にプロビジョニングできる設計上、「ファイル A にないルール」がファイル B で管理されているかもしれず、Grafana にはどのファイルが何を管理しているか判断できません。また設定ファイルのバグやマウント失敗で一時的にファイルが読めなくなったとき、自動削除だとアラートルールが全消えするリスクもあります。Terraform が管理対象を state ファイルで追跡しているから自動削除できるのと対照的で、file-based provisioning にはその state 概念がないため、削除の意図を明示する仕組みとして deleteRules が用意されています。 ルールを削除するには以下のように UID を指定します。 alerting : rules.yaml : groups : - ... deleteRules : - orgId : 1 uid : hoge-alert # 削除したいルールの UID を明示 6. 専用 NodePool への分離 Prometheus・Loki・Grafana・Alloy といった観測性コンポーネントを、Runner と同じ NodePool に混在させていると2つのリスクがあります。 1つ目は、Prometheus の OOM のような観測性コンポーネントのリソースプレッシャーが同じノード上のランナーの動作に悪影響を与えるリスクです。 2つ目は、Karpenter の consolidation に巻き込まれるリスクです。runner が使う NodePool は業務時間帯の consolidation を無効にしており、早朝の限られた時間帯のみ有効になる設定にしています。この時間帯に consolidation が走ったとき、同じ NodePool にいる観測性コンポーネントの Pod が別ノードに移動させられ、メトリクスやログが欠損するリスクがあります。 専用 NodePool に分離することで、これらの問題をランナーから切り離せます。 Karpenter で dedicated=observability:NoSchedule の taint を付けた専用 NodePool を用意し、Prometheus・Loki・Grafana などの Deployment / StatefulSet 系コンポーネントに対応する toleration と nodeSelector を設定しました。なお、DaemonSet である Alloy と node-exporter は全ノードで動かす必要があるため toleration のみ付与しています。 # NodePool taints : - key : dedicated value : observability effect : NoSchedule # 各コンポーネント tolerations : - key : dedicated value : observability effect : NoSchedule nodeSelector : karpenter.sh/nodepool : observability 監視用コンポーネントについてもランナーと同じく、arm64 on-demand インスタンスを使用するようにしています。 ただし、インスタンスタイプの指定が甘いと Karpenter がコンピュート最適化インスタンス(c6g.large / 4GB RAM)を選んでしまい、Loki や Grafana が OOMKill されるという問題が起きてしまいます。observability スタックのメモリ使用量を実際に確認したうえで、NodePool の requirements に 8GB 以上のインスタンスを指定するなど、きちんとメモリ要件に合ったインスタンスが選ばれるように注意が必要です。 requirements : - key : eks.amazonaws.com/instance-memory operator : Gt values : [ "7168" ] # 8GB 以上 7. まとめ Grafana・Prometheus・Loki・Alloy を Self-hosted Runner の監視基盤として導入するにあたり、各コンポーネントで様々なハマりポイントがありました。特に Prometheus の OOM は複数の原因が重なっており、1つ解決しても次の問題が出てくる形で対応に時間がかかりました。また EKS Auto Mode には通常の EKS(マネージドノードグループ等)と設定において異なる部分があるため、EKS Auto Mode に初めて触る方や移行を考えている方は特に注意してください。 同じ構成を検討している方の参考になれば幸いです。
はじめに CIU(CyberAgent group Infrastructure Unit)で Ia ...

















