🚜

EKS Fargateのtelemetryデータをopentelemetry-collectorで取得する

2024/07/17に公開

tl;dr

  • EKS Fargateで動かすapplication containerのtelemetryデータ(metrics, logs, traces)をsidecarのopentelemetry-collector(以下sidecar otel-col)にて取得した
    • metrics
      • sidecar otel-colにてnode/proxy apiを利用しkubeletstat receiverで取得
        • 動的に決まるnode名をDownward APIで取得
    • logs
      • teeコマンドにて標準出力とemptyDirをマウントした先にファイル出力
      • sidecar otel-colにて同一emptyDirをマウントしてfilelog receiverで取得
        • ファイル出力によるdiskfullを防ぐためlogrotate containerもsidecarとして追加
      • logsに付与したいpod名、namespace名等をDownward APIで取得
    • traces
      • sidecar otel-colにてotlp receiverでデータを待ち受け
      • application containerはsidecar otel-colにtracesを投げる

はじめに

皆さんk8s上のシステムのtelemetryデータはどのように取得していますでしょうか。
DatadogのようなSaaSを使ったり、各クラウドベンダが提供するサービス(AWS/Amazon EKSだとContainer Insightsなど)を利用したり様々な方法があるかと思っています。

弊社ではAmazon EKSや閉域オンプレミス上のk8sクラスタなど、複数のk8sクラスタを運用する機会があり、
telemetryデータの取得方法をできるだけ統一したくopentelemetry-collector(以降 otel-col)の利用へと切り替えています。

オンプレミスでの対応を先に行い、その後EKSへと同様の設定で展開しようとしたところでFargateでのtelemetryデータの取得に詰まってしまったため、Fargate用の対応を行いました。

どういった問題が発生したのか、どう解決したのかについて纏めます。

前提

  • otel-colはopentelemetry-collector operatorでdeploy
  • versionは0.102.0を使用
  • 以下設定等についてはconfigや必要な部分を抜粋して記載
  • オブザーバビリティバックエンドについては言及しない
    • telemetryデータの取得について注力します!

オンプレミスk8sでのtelemetryデータの取得

オンプレミスでのtelemetryデータの取得についてmetrics, logs, tracesそれぞれの方式をconfigを交えて記載します。

metrics

otel-colのkubeletstat receiverを使い、DaemonSetでnodeごとに取得しました。

apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
spec:
  mode: daemonset
  env:
  - name: NODE_IP
    valueFrom:
      fieldRef:
        fieldPath: status.hostIP
  config: |
    receivers:
      kubeletstats:
        auth_type: serviceAccount
        endpoint: "https://${NODE_IP}:10250"

DaemonSetが動くNodeのkubelet apiを叩いてmetrics情報を取得しています。

/stats/summary/pods apiを使っています。

https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/v0.102.0/receiver/kubeletstatsreceiver/internal/kubelet/rest_client.go#L28-L34

logs

otel-colのfilelog receiverを使い、DaemonSetでnodeごとに取得しました。

apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
spec:
  mode: daemonset
  volumes:
  - name: varlog        
    hostPath:          
      path: /var/log
  volumeMounts:
  - mountPath: /var/log          
    name: varlog          
    readOnly: true
  config: |
    receivers:
      filelog:
        include: /var/log/pods/*/*/*.log
        exclude: /var/log/pods/monitoring_*/*/*.log
        include_file_path: true
        operators:
        - type: regex_parser
          id: parser-containerd
          regex: '^(?P<time>[^ ^Z]+Z) (?P<stream>stdout|stderr) (?P<logtag>[^ ]*) ?(?P<log>.*)$'
          output: extract_metadata_from_filepath
          timestamp:
            parse_from: attributes.time
            layout: '%Y-%m-%dT%H:%M:%S.%LZ'
      - type: regex_parser
        id: extract_metadata_from_filepath
        regex: '^.*\/(?P<namespace>[^_]+)_(?P<pod_name>[^_]+)_(?P<uid>[a-f0-9\-]{36})\/(?P<container_name>[^\._]+)\/(?P<restart_count>\d+)\.log$'
        parse_from: attributes["log.file.path"]
        - type: move
          from: attributes.namespace
          to: resource["k8s.namespace.name"]
        - type: move
          from: attributes.pod_name
          to: resource["k8s.pod.name"]

exampleを参考に作成しているため、filelog receiver設定の詳細はこちらを見ていただければです。

Node(ホスト)側に出力されるログをマウントしてそこを読んでいます。
そして、include_file_path: trueを設定してlog.file.path attributeにファイルパスを設定してもらい、operatorsというログ処理の機構の中でそのディレクトリ構造からnamespaceやpod名といった後でログを見る際に必要な情報を抽出してresourceに設定することをしています。

また、criが出力するtimeを見て、timestampに設定してます。
(podごとにlog formatが異なっても、criが設定するformatで必要な情報を取得できます。)

余談ですが、直近(2024/05)の更新で、
operatorsにcontainer operatorという、上記で行っているcriごとによく行われるparse対応を纏めて行ってくれるoperatorが追加されたのでそちらを使うとoperator部分が簡素にかけそうです。
まだ試せていないのですが、試して置き換えたいと思います。

上記では17行あったcontainerdに沿ったparse対応処理が以下の通り、3行くらいでかけてしまいます。
(default設定を明示的に書いている項目があったり、過不足している行があったりするので正確ではないですが、個人的にはそのくらいのインパクトでした。)

- type: container
  format: containerd
  add_metadata_from_filepath: true

これでcontainerd形式のファイルを読み込み、時刻、body、stdout/errの抽出とfile pathからpod名やnamespace名などの抽出を行ってくれるようです。

これからfilelog receiverを使い始める方はこのcontainer operatorを使うのが良さそうです。

traces

otel-colのotlp receiverを使い、sidecarでapplication podごとに取得しました。

apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
spec:
  mode: sidecar
  config: |
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: "127.0.0.1:4317"

sidecarではgrpcにて待ち受けて、otlpのtracesをapplicationが投げてくるのを待つだけです。

EKSへの適用/Fargateでの問題

オンプレでは問題なく動き、いざ上記設定をEKSへ適用/展開しようとしました。

するとEKS on EC2は問題なく動くのですが、
EKS FargateはDaemonSetが使えない[1]ため上記の方法ではmetrics, logの取得ができませんでした。
tracesの取得はsidecarで行っているため変わらず可能です。

Fargateは1pod = 1nodeのため、単純にsidecarにすれば同じ方式でできるのでは?とも思いましたが、以下の点があり何か別の手を検討する必要がありました。

  1. kubelet apiが対象node上からは叩け無い[2]
  2. criがnodeに配置するログファイルをcontainerからmountできない[3]

メンテナンスなどを考慮してできるだけ同じreceiverを使いたい思いがありその方向で色々と試してみたところ、妥協した部分はありますが、以下の方法で解決できました。

EKS Fargateでのtelemetryデータの取得

tracesはオンプレミスk8sでのtelemetryデータの取得と同様のため、
metrics, logsについてFargate用に修正した方式を記載します。

metrics

nodes/proxy apiを利用することにしました。

apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
spec:
  mode: sidecar
  env:
  - name: NODE_NAME
    valueFrom:
      fieldRef:
        fieldPath: spec.nodeName
  config: |
    receivers:
      kubeletstats:
        auth_type: serviceAccount
        endpoint: "https://kubernetes.default.svc:443/api/v1/nodes/${env:NODE_NAME}/proxy"

podから直接kubelet apiを叩け無いため、api-serverを経由して叩こうという作戦です。
その際に、ノード名が必要になるためDownward APIで取得しています。

nodes/proxy apiを使うために権限を付与する必要があります。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
rules:
  - apiGroups: [""]
    resources:
      - nodes/proxy
    verbs: ["get"]

かなりすっきりkubeletstatsでfargate podのmetricsを取ることができました。

蛇足ですが、1Podあたり1ServiceAccount(SA)になるため、メインのapplication contianerとsidecar container共通で利用するSAに権限付与する必要があるのが少し気持ち悪いと思いました。

この件については議論[4]があるようですね。

logs

criがnodeに配置するログファイルを読むことは諦めて、自前でログファイルに出力する方式にしました。
kubectl logsコマンドは引き続き利用できると嬉しいため標準出力にも出力します。

そこでteeコマンドを使いました。

otel-colのmanifestより先にfargateで動かすアプリmanifest(Deployment)のイメージを記載します。

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - name: application
        command:
        - /bin/bash
        args:
        - -c
        - exec_file | tee -a /var/log/fargate/application.log
        volumeMounts:
        - mountPath: /var/log/fargate
          name: fargate-app-logs
      - name: logrotate
        volumeMounts:
        - mountPath: /var/log/fargate
          name: fargate-app-logs
      volumes:
      - emptyDir: {}
        name: fargate-app-logs

妥協した点なのですが、標準エラー出力については取得できていません。

原因をわかっていないのですが、標準エラー出力も取得を試みると、
標準出力と標準エラー出力の出力順序がおかしくなるケースが有り、そこの時系列が乱れると調査がしにくくなると判断し、かつ標準エラーの場合は別の検知が可能であったためです。

こちらも蛇足ですが、このようなコマンドを試していました。

exec_file 1> >(tee stdout.log >&1 ) 2> >(tee stderr.log >&2)

また、自前で作成したログファイルはもちろんkubeletによるrotateやディレクトリ構造からのnamespace, pod名の判別ができなくなる[5]ためそれらに対応する必要があります。

実際にdiskfullになるとどうなるか試してみたところprocess(pod)は落ちないものの、自前のログファイルや(恐らく)criが作成するログファイルへの出力ができなくなり、まったくログが見れないpodが出来上がりました。(kubectl logsも追加のログがまったくない)

これを避けるために、さらにsidecarでlogrotateを実行しています。

こちら[6]を参考にさせていただきました。

長くなりましたが以上がアプリでのログファイル作成の部分で以降がote-colでのログファイル読み取りの話です。

apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
spec:
  mode: sidecar
  volumeMounts:
  - name: fargate-app-logs
    mountPath: /var/log/fargate
    readOnly: true
  env:
  - name: POD_NAME
    valueFrom:
      fieldRef:
        fieldPath: metadata.name
  - name: POD_NAMESPACE
    valueFrom:
      fieldRef:
        fieldPath: metadata.namespace
  - name: POD_UID
    valueFrom:
      fieldRef:
        fieldPath: metadata.uid
  config: |
    receivers:
      filelog:
        include: /var/log/fargate/application.log
        include_file_path: false
        resource:
          k8s.container.name: fargate-application
          k8s.namespace.name: ${env:POD_NAMESPACE}
          k8s.pod.name: ${env:POD_NAME}
          k8s.pod.uid: ${env:POD_UID}
        operators:
        - type: json_parser
          timestamp:
            parse_from: attributes.timestamp
            layout: '%Y-%m-%dT%H:%M:%S.%LZ'

いくつかポイントがあるので箇条書きで並べると

  • アプリが作成するログを読むためにvolumeMountをする
    • sidecarからvolumeMountするのが初だったのでvolumes宣言がないけど、volumeMountを書くのに違和感があった
  • criによりpod名、namespaceといった情報はDownward APIから取得する
  • criが付与してくれていたtimestamp情報はアプリのログから取得する
    • sidecarなのでログの形式はわかっている

という感じです。

こちらは色々手が入りましたが、無事filelog receiverでfargate podのログを取得することができました。

纏め

EKS Fargateの制約/特性上、onpreと同様にDaemonSetでmetrics/logsを取得することはできませんでしたが、

  • metrics
    • nodes/proxyを利用して無理やりapi-server経由でkubeletのstats apiを叩く
  • logs
    • 自前でログファイルに出力する
    • logrotateを一緒に動かす

という対応をしてsidecarで動かすことでなんとかFargateでもtelemetryデータの取得ができるようになりました。
また、同じreceiver(kubeletstats, filelog)を使うことができたため、後段の処理は同じものを使うことができます。

ただ、logsについて標準エラー出力の扱いで妥協した部分があるため、その点は意識する必要があります。
なにか良い方法が見つかれば追記できればと思います。

最後まで見ていただきましてありがとうございます!!
気になる点などございましたら教えていただければとても嬉しいです!
ありがとうございました。

脚注
  1. DaemonSets aren’t currently supported. from https://docs.aws.amazon.com/prescriptive-guidance/latest/implementing-logging-monitoring-cloudwatch/kubernetes-eks-metrics.html ↩︎

  2. https://github.com/aws/containers-roadmap/issues/1798 ↩︎

  3. HostPath persistent volumes aren't supported. from: https://docs.aws.amazon.com/prescriptive-guidance/latest/implementing-logging-monitoring-cloudwatch/kubernetes-eks-metrics.html ↩︎

  4. https://github.com/kubernetes/kubernetes/issues/66020 ↩︎

  5. kubeletはログのローテーションとログディレクトリ構造の管理を担当します。 from: https://kubernetes.io/ja/docs/concepts/cluster-administration/logging/ ↩︎

  6. https://github.com/4406arthur/logrotate ↩︎

FRAIMテックブログ

Discussion