📚

EKS on Fargate で ADOT を使用した Container Insights の取得

2024/07/05に公開

この記事のゴール

EKS on Fargate で AWS Distro for OpenTelemetry(以下、ADOT) を使用し、CloudWatch Container Insighs が使用できるようにします。

こちらのブログに詳細な方法が記録されており、こちらを元に進めていきます。

https://aws.amazon.com/jp/blogs/news/introducing-amazon-cloudwatch-container-insights-for-amazon-eks-fargate-using-aws-distro-for-opentelemetry/

EKS on Fargate における Container Insights について

まず Contianer Insights について、簡単にまとめます。

Container Insights は、埋め込みメトリクス形式(EMF)を使用して、パフォーマンスログイベントとしてデータを収集します。
このパフォーマンスログから、EKS の場合はクラスター、ノード、ポッド、タスク、サービスのレベルで CloudWatch メトリクスとして集約されたメトリクスを作成します。Container Insights が収集するメトリクスは、CloudWatch 自動ダッシュボードで使用でき、CloudWatch コンソールの [メトリクス] セクションでも表示できます。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/ContainerInsights.html

EKS ではさらに、 Amazon EKS 向けにオブザーバビリティが強化された Container Insights というものがあり、コンテナレベルまでの詳細なヘルス、パフォーマンス、ステータスのメトリクスだけでなく、コントロールプレーンのメトリクスも収集できます。
こちらの機能を使用するには、Amazon CloudWatch Observability EKS アドオンまたは CloudWatch エージェントを使用する必要があります。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/ContainerInsights.html#container-insights-detailed-metrics

Fargate で Container Insights を利用する場合、AWS Distro for OpenTelemetry を使用する必要があります。
「Amazon EKS 向けにオブザーバビリティが強化された Container Insights」は、Fargate ではサポートされていません。

アーキテクチャ

冒頭のブログより

Kubernetes クラスターのワーカーノード上の kubelet は、CPU、メモリ、ディスク、ネットワーク使用量などのリソースメトリクスを /metrics/cadvisor エンドポイントで公開しています。
しかし、EKS Fargate のネットワーキングアーキテクチャでは、Pod はそのワーカーノード上の kubelet に直接アクセスすることを許可されていません。
したがって、ADOT Collector は Kubernetes API サーバーを呼び出して、ワーカーノード上の kubelet への接続をプロキシし、そのノード上のワークロードの kubelet の cAdvisor メトリクスを収集します。

Fargate の場合、基盤となるホストは AWS 管理となるためにワーカーノード上の kubelet へアクセスすることができないようになっています。
そのため、直接 kubelet へメトリクスを収集するのではなく、Kubernetes API サーバー経由で各ワーカーノードの kubelet へアクセスして、cAdvisor メトリクスを収集するというアーキテクチャとなります。

Kubernetes のサービスディスカバリーを使用して、Receiver は EKS クラスター内のすべてのワーカーノードを検出することができます。
したがって、クラスター内のすべてのノードからリソースメトリクスを収集するには、ADOT Collector の 1 つのインスタンスで十分です。

Prometheus Receiver から他のワーカーノード(Fargate)の検出もできると記載があります。
そのため、各ワーカーノードに ADOT Collector Pod を配置する必要はないということですね。

前提条件

当方の環境は、Kubernetes バージョン 1.29 のクラスターを使用しました。

ADOT Collector をデプロイするにあたり、いくつか前提条件があります。

1. Fargate Pod 実行ロールの準備

Fargate 上で起動する Pod から AWS API を呼び出す際に必要となるロールです。
ECR からコンテナイメージをプルしたりする際は、この Pod 実行ロールが使用されます。

--fargate オプションを使用して eksctl でクラスターを作成した場合は、クラスターに Pod 実行ロールが「eksctl-<my-cluster>-FargatePodExecutionRole-<ABCDEFGHIJKL>」という形式で自動的に作成されます。
eksctl を使用して Fargate プロファイルを作成する場合でも、自動的に作成してくれます。

https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/fargate-getting-started.html#fargate-sg-pod-execution-role

2. Fargate プロファイルの準備

Fargate で実行される Pod をスケジューリングするのに、Fargate プロファイルが必要となります。

今回のブログでは、ADOT Collector とサンプルアプリがデプロイされます。
それぞれ Namespace:fargate-container-insights, golang でデプロイされるようになっています。
各自の環境に合わせて修正いただければと思いますが、当該ブログにある通りにやる場合は以下の2つのプロファイルを作成します。

Profile-1
ProfileName : fargate-container-insights
Namespace : fargate-container-insights
Profile-2
ProfileName : applications
Namespace : golang

3. CWlogsへ送るため、ServiceAccount を IAM ロールに関連づける

awsemf Exporter を使用しているため、ADOT Collector からパフォーマンスログを CloudWatch Logs へ送信します。
この時使用される IAM ロールを設定するのに、IAM Roles for Service Accounts (IRSA) 機能を使って、Kubernetes のサービスアカウント (Service Account) を IAM ロールに関連付けることで実現できます。

当該ブログ内で、ヘルパースクリプトが用意されているためこちらを使用します。

#!/bin/bash
CLUSTER_NAME=YOUR-EKS-CLUSTER-NAME
REGION=YOUR-EKS-CLUSTER-REGION
SERVICE_ACCOUNT_NAMESPACE=fargate-container-insights
SERVICE_ACCOUNT_NAME=adot-collector
SERVICE_ACCOUNT_IAM_ROLE=EKS-Fargate-ADOT-ServiceAccount-Role
SERVICE_ACCOUNT_IAM_POLICY=arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy

// クラスターの IAM OIDC プロバイダーを作成
eksctl utils associate-iam-oidc-provider \
--cluster=$CLUSTER_NAME \
--approve

// ServiceAccount と IAM ロールを関連づける
eksctl create iamserviceaccount \
--cluster=$CLUSTER_NAME \
--region=$REGION \
--name=$SERVICE_ACCOUNT_NAME \
--namespace=$SERVICE_ACCOUNT_NAMESPACE \
--role-name=$SERVICE_ACCOUNT_IAM_ROLE \
--attach-policy-arn=$SERVICE_ACCOUNT_IAM_POLICY \

ADOT Collector の設定ファイル

マニフェストファイルは冒頭ブログ中盤にあるものをご確認ください。
変更したところや、何を設定しているのかを抜粋して記録します。

ClusterRole, ClusterRoleBinding

メトリクスを収集するために必要な権限が設定されています。

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: adotcol-admin-role
rules:
  - apiGroups: [""]
    resources:
      ...
    verbs: ["get", "list", "watch"]
  - nonResourceURLs: [ "/metrics/cadvisor"]
    verbs: ["get", "list", "watch"]

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: adotcol-admin-role-binding
subjects:
  - kind: ServiceAccount
    name: adot-collector
    namespace: fargate-container-insights
roleRef:
  kind: ClusterRole
  name: adotcol-admin-role
  apiGroup: rbac.authorization.k8s.io

Receiver

ADOT Collector の設定は ConfigMap で行われています。

Prometheus Receiver を使用し、Node をスクレイプ対象としています。

apiVersion: v1
kind: ConfigMap
metadata:
  name: adot-collector-config
  namespace: fargate-container-insights
  labels:
    app: aws-adot
    component: adot-collector-config
data:
  adot-collector-config: |
    receivers:
      prometheus:
        config:
          global:
            scrape_interval: 1m
            scrape_timeout: 40s

          scrape_configs:
          - job_name: 'kubelets-cadvisor-metrics'
            sample_limit: 10000
            scheme: https

            kubernetes_sd_configs:
            - role: node
            tls_config:
              ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
            bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token

relabel_configs セクションでは、Node の Label をメトリクスのラベルとして設定したり、スクレイプ対象のアドレス、メトリクスパスを変更しています。

            relabel_configs:
              - action: labelmap
                regex: __meta_kubernetes_node_label_(.+)
              - target_label: __address__
                # Changes the address to Kube API server's default address and port
                replacement: kubernetes.default.svc:443
              - source_labels: [__meta_kubernetes_node_name]
                regex: (.+)
                target_label: __metrics_path__
                # Changes the default metrics path to kubelet's proxy cadvdisor metrics endpoint
                replacement: /api/v1/nodes/$${1}/proxy/metrics/cadvisor

次に metric_relabel_configs セクションですが、ここが何をやっているのかいまいちわからずでした。

            metric_relabel_configs:
              # extract readable container/pod name from id field
              - action: replace
                source_labels: [id]
                regex: '^/machine\.slice/machine-rkt\\x2d([^\\]+)\\.+/([^/]+)\.service$'
                target_label: rkt_container_name
                replacement: '$${2}-$${1}'
              - action: replace
                source_labels: [id]
                regex: '^/system\.slice/(.+)\.service$'
                target_label: systemd_service_name
                replacement: '$${1}'

rkt とは過去に Kubernetest のコンテナランタイムで使用されていたもののようです。
https://github.com/rkt/rkt

後者は systemd で実行されているサービスということでしょうか。
今回取得してきたメトリクスにはこれらに該当するメトリクスは確認できておりません。

Processor

metricstransform

ラベルのRenameを行っています。
experimental_match_labels: {"container": "\\S", "LaunchType": "fargate"} では、container ラベルが存在し、LaunchType=fargate となっているメトリクスのみを変更しています。

    processors:
      # rename labels which apply to all metrics and are used in metricstransform/rename processor
      metricstransform/label_1:
        transforms:
          - include: .*
            match_type: regexp
            action: update
            operations:
              - action: update_label
                label: name
                new_label: container_id
              - action: update_label
                label: kubernetes_io_hostname
                new_label: NodeName
              - action: update_label
                label: eks_amazonaws_com_compute_type
                new_label: LaunchType

      # rename container and pod metrics which we care about.
      # container metrics are renamed to `new_container_*` to differentiate them with unused container metrics
      metricstransform/rename:
        transforms:
          - include: container_spec_cpu_quota
            new_name: new_container_cpu_limit_raw
            action: insert
            match_type: regexp
            experimental_match_labels: {"container": "\\S", "LaunchType": "fargate"}
          - include: container_spec_cpu_shares
            new_name: new_container_cpu_request
            action: insert
            match_type: regexp
            experimental_match_labels: {"container": "\\S", "LaunchType": "fargate"}
            ...

filter

new_container_,pod_ から始まるメトリクスのみをフィルタリングしています。
ここで取得したいメトリクスを絞る設定ができそうです。

      # filter out only renamed metrics which we care about
      filter:
        metrics:
          include:
            match_type: regexp
            metric_names:
              - new_container_.*
              - pod_.*

cumulativetodelta, deltatorate

cumulativetodelta について、ブログの書き方と最新バージョンの書き方が異なるようで、少々手直しが必要でした。

https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/cumulativetodeltaprocessor/README.md#description

この2つの processor の役割としては、以下の2つと理解しています。

  • cumulativetodelta : 累積値のメトリクスをデルタ値(増分値)に変換
  • deltatorate : デルタ値をレート値(単位時間あたりの値)に変換
      # convert cumulative sum datapoints to delta
      cumulativetodelta:
        include:
          match_type: strict
          metrics:
            - new_container_cpu_usage_seconds_total
            - pod_cpu_usage_seconds_total
            - pod_memory_pgfault
            - pod_memory_pgmajfault
            ...

      # convert delta to rate
      deltatorate:
        metrics:
          - new_container_cpu_usage_seconds_total
          - pod_cpu_usage_seconds_total
          - pod_memory_pgfault
          - pod_memory_pgmajfault
          ...

experimental_metricsgeneration

既存のメトリクスから計算などを行い、新しくメトリクスを作成します。

https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/metricsgenerationprocessor

「CPU の使用率」などはメトリクスとして用意されていないため、それをこの processor で作成しています。

      experimental_metricsgeneration/1:
        rules:
          - name: pod_network_total_bytes
            unit: Bytes/Second
            type: calculate
            metric1: pod_network_rx_bytes
            metric2: pod_network_tx_bytes
            operation: add
          ...

      experimental_metricsgeneration/2:
        rules:
          - name: pod_cpu_utilization_over_pod_limit
            type: calculate
            unit: Percent
            metric1: pod_cpu_usage_total
            metric2: pod_cpu_limit
            operation: percent

metricstransform(2回目)

最後にメトリクスのラベル付けとして、必要なラベルを付与しています。
上記の複数 processor により作成・変更したメトリクスに対しても、最終的にラベル付けをしているようです。

      # add `Type` and rename metrics and labels
      metricstransform/label_2:
        transforms:
          - include: pod_.*
            match_type: regexp
            action: update
            operations:
              - action: add_label
                new_label: Type
                new_value: "Pod"
          - include: new_container_.*
            match_type: regexp
            action: update
            operations:
              - action: add_label
                new_label: Type
                new_value: Container
          ...

resourcedetection

環境変数 OTEL_RESOURCE_ATTRIBUTES と、EKSメタデータ(EKSクラスター名など)を取得しています。
今回の場合、StatefulSet の中で OTEL_RESOURCE_ATTRIBUTES として ClusterName=YOUR-EKS-CLUSTER-NAME を設定しています。これでクラスター名を取得しているようです。
https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/resourcedetectionprocessor

      # add cluster name from env variable and EKS metadata
      resourcedetection:
        detectors: [env, eks]

host で OpenTelemetry Collector 実行している場合などは host の情報も取得できる processor のようです。

Exporter

awsefm exporter で CloudWatch へメトリクスを送っています。

    exporters:
      awsemf:
        log_group_name: '/aws/containerinsights/{ClusterName}/performance'
        log_stream_name: '{PodName}'
        namespace: 'ContainerInsights'
        region: YOUR-AWS-REGION
        resource_to_telemetry_conversion:
          enabled: true
        eks_fargate_container_insights_enabled: true
        parse_json_encoded_attr_values: ["kubernetes"]
        dimension_rollup_option: NoDimensionRollup
        metric_declarations:
          - dimensions: [ [ClusterName, LaunchType], [ClusterName, Namespace, LaunchType], [ClusterName, Namespace, PodName, LaunchType]]
            metric_name_selectors:
              - pod_cpu_utilization_over_pod_limit
              - pod_cpu_usage_total
              - pod_cpu_limit
              - pod_memory_utilization_over_pod_limit
              - pod_memory_working_set
              - pod_memory_limit
              - pod_network_rx_bytes
              - pod_network_tx_bytes

気になるオプションを個別に見ていきます。

eks_fargate_container_insights_enabled について
awsemfexporter の README.md にはこのオプションを見つけられませんでしたが、config.go の方に記載がありました。
https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/af3f3ff49c6d5fbf035412360ba4003e0ec5460c/exporter/awsemfexporter/config.go#L76

// EKSFargateContainerInsightsEnabled is an option to reformat certin metric labels so that they take the form of a high level object
// The end result will make the labels look like those coming out of ECS and be more easily injected into cloudwatch
// Note that at the moment in order to use this feature the value "kubernetes" must also be added to the ParseJSONEncodedAttributeValues array in order to be used
EKSFargateContainerInsightsEnabled bool mapstructure:"eks_fargate_container_insights_enabled"

メトリクスラベルを CloudWatch へ送るのに必要なオプションなのでしょうか?
ちょっとこの説明からだけでは挙動がイメージできません。
parse_json_encoded_attr_values: ["kubernetes"] とセットである必要があるとのこと。

metric_declarations について
CloudWatch へ送るメトリクス、ディメンションを指定できます。
これで監視したいメトリクスを選択したり、ディメンションをカスタマイズできます。

ADOT Collector を StatefulSet としてデプロイ

aws-otel-collector のイメージタグを最新の v0.40.0 に変更しています。

apiVersion: v1
kind: Service
metadata:
  name: adot-collector-service
  namespace: fargate-container-insights
  ...
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: adot-collector
  namespace: fargate-container-insights
  labels:
    app: aws-adot
    component: adot-collector
spec:
  ...
  template:
    metadata:
      ...
    spec:
      serviceAccountName: adot-collector
      securityContext:
        fsGroup: 65534
      containers:
        - image: amazon/aws-otel-collector:v0.40.0
          name: adot-collector
          ...

サンプルアプリのデプロイ

今回のブログでは、Namespace : golangにアプリをデプロイしようとしています。
Namespace が作成されていないと思うので、一緒に作成します。

---
apiVersion: v1
kind: Namespace
metadata:
  creationTimestamp: null
  name: golang
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
  namespace: golang
spec:
  replicas: 2
  ...
  template:
    ...
    spec: 
      containers:          
        - name: go  
          image: public.ecr.aws/awsvijisarathy/prometheus-webapp:latest
          ...

このアプリが何を行うアプリなのかがわかりませんでした。
image 自体は公開されているものの、詳細な仕様は確認できませんでした。
まぁ取得しているメトリクスは cAdvisor から取得しているメトリクスですし、独自メトリクスを取得しようとしているわけでもないため、特に問題ないでしょう。
https://gallery.ecr.aws/awsvijisarathy/prometheus-webapp

結果

CloudWatch のマネジメントコンソールを確認してみましょう。

ContainerInsights というカスタムメトリクスが作成されています。
こちらを見てみると、awsemf Exporter で指定しているディメンションが作成されています。

メトリクスを見てみると、メトリクスが取れていることがわかります。
Namespace ごと、Pod ごとのメトリクスが取得できています。

続いて、[Container Insights] > [コンテナマップ] を確認してみます。
CPU % によって、色が 緑 -> 黄 -> 赤 と変化してくれるようです。

[Container Insights] > [パフォーマンスのモニタリング] を確認してみると、ダッシュボードも用意されています。
名前空間、サービスなどでフィルタリングもできます。

終わりに

EKS on Fargate で AWS Distro for OpenTelemetry(以下、ADOT) を使用し、CloudWatch Container Insighs が使用できるようになりました。
ブログにある通りの手順で実施しましたが、以下の疑問点があります。こちらは宿題にしたいと思います。

Q: アドオンで入れられる?

EKS では ADOT アドオンが用意されており、ADOT Operator をインストールすることが可能です。
EKS on Fargate でもおそらくできるのかと思います。
https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/opentelemetry.html

注意点として、cert-manager が必要となります。
Fargate の場合、kubelet がデフォルトで 10250 ポートを使用している関係で、cert-manager webhook で使用するポートを変更する必要があります。

https://cert-manager.io/docs/installation/compatibility/

If you're using AWS Fargate or else if you've specifically configured cert-manager to run the host's network, be aware that kubelet listens on port 10250 by default which clashes with the default port for the cert-manager webhook.

As such, you'll need to change the webhook's port when setting up cert-manager.

For installations using Helm, you can set the webhook.securePort parameter when installing cert-manager either using a command line flag or an entry in your values.yaml file.

If you have a port clash, you could see confusing error messages regarding untrusted certs. See #3237 for more details.

Discussion