EKS で CloudWatch Application Signals を有効化してみた

EKS で CloudWatch Application Signals を有効化してみた

こんにちは。クラウド事業本部の枡川です。
今回は EKS で CloudWatch Application Signals(以降 Application Signals) を有効化してみました。

Application Signals とは

OpenTelemetry 互換のアプリケーションパフォーマンスモニタリングを実現するためのサービスです。
アプリケーションの自動計測をセットアップして、各種メトリクスを分析するためのダッシュボードを用意して、各種メトリクスから SLO を定義してアラームを設定して、といった一連の流れをベストプラクティスに沿った形で比較的簡単に行うことができます。

https://dev.classmethod.jp/articles/amazon-cloudwatch-application-signals-ga/

EKS で利用する際と ECS で利用する際の違い

ECS on Fargate を利用している場合、都度サイドカーの追加や環境変数の設定などが必要になります。
環境変数としては下記のようなものを指定します。

環境変数名 内容
OTEL_RESOURCE_ATTRIBUTES サービス名や環境名など
OTEL_AWS_APPLICATION_SIGNALS_ENABLED X-Ray トレースと CloudWatch メトリクスを Application Signals に送信するかどうか
OTEL_METRICS_EXPORTER 他のメトリクスエクスポーターを利用するかどうか
OTEL_LOGS_EXPORTER 他のログエクスポーターを利用するかどうか
OTEL_EXPORTER_OTLP_PROTOCOL OTLP トランスポートプロトコルで Application Signals では http/protobuf を選択する
OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT メトリクスの送信先で http://localhost:4316/v1/metrics を指定する
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT メトリクスの送信先で http://localhost:4316/v1/traces を指定する
OTEL_TRACES_SAMPLER サンプラーで X-Ray のサンプリングルールを利用するなら xray を指定
OTEL_PROPAGATORS プロパゲーターで xray を指定
JAVA_TOOL_OPTIONS Java の自動計装エージェントのパス

また、Java の場合は自動計装エージェントを Java エージェントとしてコンテナ内に仕込む必要があります。

https://github.com/masutaro99/spring-boot-fargate/blob/main/app/Dockerfile

ある程度定型化された作業を行うだけではありますが、それなりに手間です。
EKS で CloudWatch Observability アドオンを利用している場合、この辺りのインスツルメントは自動で行ってくれます。

EKS の場合の自動インスツルメント

マネジメントコンソールから設定する方法と、Kubernetes のマニフェストファイルにアノテーションをつける方法があります。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch-Application-Signals-Enable-EKS.html

マネジメントコンソールからの設定も便利ではありますが、どのサービスで Application Signals を有効化するかをコード管理しようと思った場合、マニフェストファイル側で管理した方がやりやすいと思うので、今回はアノテーションをつける方法を試してみます。

※ マネジメントコンソールの場合、アプリケーションをデプロイした後に名前空間や deployment を選択して有効化する形になります。

management.png

EKS クラスターの作成

各種リソースは Terraform の AWS EKS Terraform module で作ります。
EKS Auto Mode を利用します。
また、Observability アドオンも EKS Pod Identity に対応しているので、こちらで権限を付与します。

https://dev.classmethod.jp/articles/cloudwatch-observability-addon-pod-identity/

権限としては CloudWatchAgentServerPolicyAWSXRayWriteOnlyAccess を付与します。

locals {
  cluster_name                  = "test-cluster"
  k8s_service_account_namespace = "amazon-cloudwatch"
  k8s_service_account_name      = "cloudwatch-agent"
}

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.36.0"

  cluster_name                    = local.cluster_name
  cluster_version                 = "1.32"
  cluster_endpoint_public_access  = true
  cluster_endpoint_private_access = true

  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets

  enable_cluster_creator_admin_permissions = true

  cluster_compute_config = {
    enabled    = true
    node_pools = ["general-purpose"]
  }

  bootstrap_self_managed_addons = false

  cluster_addons = {
    "observability" = {
      name    = "amazon-cloudwatch-observability"
      version = "v4.0.0"
      pod_identity_role_arn = {
        role_arn        = aws_iam_role.observability_addon.arn
        service_account = local.k8s_service_account_name
      }
    }
  }
}

data "aws_iam_policy_document" "allow_pod_identity" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["pods.eks.amazonaws.com"]
    }

    actions = [
      "sts:AssumeRole",
      "sts:TagSession"
    ]
  }
}

resource "aws_iam_role" "observability_addon" {
  name               = "AmazonCloudWatchObservabilityAddonRole"
  assume_role_policy = data.aws_iam_policy_document.allow_pod_identity.json
}

resource "aws_iam_role_policy_attachment" "observability_addon_cloudwatch" {
  role       = aws_iam_role.observability_addon.name
  policy_arn = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy"
}

resource "aws_iam_role_policy_attachment" "observability_addon_xray" {
  role       = aws_iam_role.observability_addon.name
  policy_arn = "arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess"
}

resource "aws_eks_pod_identity_association" "observability_addon" {
  cluster_name    = module.eks.cluster_name
  namespace       = local.k8s_service_account_namespace
  service_account = local.k8s_service_account_name
  role_arn        = aws_iam_role.observability_addon.arn
}

Application Signals を有効化

Application Signals はマネジメントコンソールから有効化していきます。
「Application Signals を有効にする」をクリックします。

application-signals0.png

プラットフォームとして EKS を選択します。

application-signals1.png

「マニフェストフィアルに注釈を付ける」を選択します。

applications-signals2.png

プログラミング言語ごとの注釈の付与方法が表示されます。

applicaton-signals3.png

「完了」をクリックして、クラスターで Application Signals を有効化します。

applications-signals4.png

アプリケーションのデプロイ

アノテーション (注釈) を付与してアプリケーションをデプロイします。
Spring Boot のアプリケーションを作成して、下記 DockerFile でコンテナ化しました。

FROM public.ecr.aws/amazoncorretto/amazoncorretto:17aws-opentelemetry-agent.jar
COPY ./build/libs/sample-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

ECS の際は ADD https://github.com/aws-observability/aws-otel-java-instrumentation/releases/download/v1.32.6/aws-opentelemetry-agent.jar ./aws-opentelemetry-agent.jar といった行を入れて自動計装エージェントを追加しましたが、今回は不要です。

https://github.com/masutaro99/spring-boot-fargate/blob/main/app/Dockerfile

下記マニフェストファイルでデプロイします。
特筆すべき点は instrumentation.opentelemetry.io/inject-java: "true" のアノテーションをつけるくらいで環境変数の指定などは不要です。

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: default
  name: spring-boot
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: spring-boot
  replicas: 1
  template:
    metadata:
      labels:
        app.kubernetes.io/name: spring-boot
      annotations:
        instrumentation.opentelemetry.io/inject-java: "true"
    spec:
      containers:
        - image: xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/spring-boot-sample-app:v3
          imagePullPolicy: Always
          name: spring-boot
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: "0.5"
          env:
            - name: DATABASE_HOST
              value: "sample-aurora-postgres-cluster.cluster-xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com"
            - name: DATABASE_NAME
              value: "postgres"
            - name: DATABASE_USER
              value: "postgres"
            - name: DATABASE_PASSWORD
              value: "password"

起動後された Pod を確認していみると、裏側で良い感じにインスツルメントしてくれていることがわかります。
Init コンテナ経由で Java の自動計装エージェントを追加しつつ、各種環境変数が設定されています。

% kubectl describe pod spring-boot-84cd88dd7-5dwck
Name:             spring-boot-84cd88dd7-5dwck
Namespace:        default
Priority:         0
Service Account:  default
Node:             i-02d6699d4ada43805/10.0.100.54
Start Time:       Fri, 23 May 2025 21:14:38 +0900
Labels:           app.kubernetes.io/name=spring-boot
                  pod-template-hash=84cd88dd7
Annotations:      instrumentation.opentelemetry.io/inject-java: true
Status:           Running
IP:               10.0.100.209
IPs:
  IP:           10.0.100.209
Controlled By:  ReplicaSet/spring-boot-84cd88dd7
Init Containers:
  opentelemetry-auto-instrumentation-java:
    Container ID:  containerd://8e5c79d379875bc9ed3052f54889bd955fcca3e7e0e636127ac765466a977cb6
    Image:         602401143452.dkr.ecr.ap-northeast-1.amazonaws.com/eks/observability/adot-autoinstrumentation-java:v2.10.0
    Image ID:      602401143452.dkr.ecr.ap-northeast-1.amazonaws.com/eks/observability/adot-autoinstrumentation-java@sha256:d16db829c68a6826c2ae28cba0feb063b48dd9c7ff434a8a4ecf4753d6d30dea
    Port:          <none>
    Host Port:     <none>
    Command:
      cp
      /javaagent.jar
      /otel-auto-instrumentation-java/javaagent.jar
    State:          Terminated
      Reason:       Completed
      Exit Code:    0
      Started:      Fri, 23 May 2025 21:14:38 +0900
      Finished:     Fri, 23 May 2025 21:14:38 +0900
    Ready:          True
    Restart Count:  0
    Limits:
      cpu:     500m
      memory:  64Mi
    Requests:
      cpu:        50m
      memory:     64Mi
    Environment:  <none>
    Mounts:
      /otel-auto-instrumentation-java from opentelemetry-auto-instrumentation-java (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-mnsvn (ro)
Containers:
  spring-boot:
    Container ID:   containerd://f1752825f80b834603beaa6d2aeca4377fae551c7c6f5662c6435b602c7a1723
    Image:          xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/spring-boot-sample-app:v3
    Image ID:       xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/spring-boot-sample-app@sha256:6b8391a294eaab0fe8f421caa53026121f2d8cd2918004429e341fe33693855e
    Port:           80/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Fri, 23 May 2025 21:14:43 +0900
    Ready:          True
    Restart Count:  0
    Requests:
      cpu:  500m
    Environment:
      DATABASE_HOST:                                   sample-aurora-postgres-cluster.cluster-xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com
      DATABASE_NAME:                                   postgres
      DATABASE_USER:                                   postgres
      DATABASE_PASSWORD:                               password
      OTEL_EXPORTER_OTLP_PROTOCOL:                     http/protobuf
      OTEL_METRICS_EXPORTER:                           none
      OTEL_LOGS_EXPORTER:                              none
      OTEL_AWS_APP_SIGNALS_ENABLED:                    true
      OTEL_AWS_APPLICATION_SIGNALS_ENABLED:            true
      OTEL_TRACES_SAMPLER_ARG:                         endpoint=http://cloudwatch-agent.amazon-cloudwatch:2000
      OTEL_TRACES_SAMPLER:                             xray
      OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:              http://cloudwatch-agent.amazon-cloudwatch:4316/v1/traces
      OTEL_AWS_APP_SIGNALS_EXPORTER_ENDPOINT:          http://cloudwatch-agent.amazon-cloudwatch:4316/v1/metrics
      OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT:  http://cloudwatch-agent.amazon-cloudwatch:4316/v1/metrics
      OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED:    true
      JAVA_TOOL_OPTIONS:                                -javaagent:/otel-auto-instrumentation-java/javaagent.jar
      OTEL_SERVICE_NAME:                               spring-boot
      OTEL_RESOURCE_ATTRIBUTES_POD_NAME:               spring-boot-84cd88dd7-5dwck (v1:metadata.name)
      OTEL_RESOURCE_ATTRIBUTES_NODE_NAME:               (v1:spec.nodeName)
      OTEL_PROPAGATORS:                                tracecontext,baggage,b3,xray
      OTEL_RESOURCE_ATTRIBUTES:                        com.amazonaws.cloudwatch.entity.internal.service.name.source=K8sWorkload,k8s.container.name=spring-boot,k8s.deployment.name=spring-boot,k8s.namespace.name=default,k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME),k8s.replicaset.name=spring-boot-84cd88dd7,service.version=v3
    Mounts:
      /otel-auto-instrumentation-java from opentelemetry-auto-instrumentation-java (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-mnsvn (ro)
Conditions:
  Type                        Status
  PodReadyToStartContainers   True
  Initialized                 True
  Ready                       True
  ContainersReady             True
  PodScheduled                True
Volumes:
  kube-api-access-mnsvn:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    ConfigMapOptional:       <nil>
    DownwardAPI:             true
  opentelemetry-auto-instrumentation-java:
    Type:        EmptyDir (a temporary directory that shares a pod's lifetime)
    Medium:
    SizeLimit:   200Mi
QoS Class:       Burstable
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                 node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  90s   default-scheduler  Successfully assigned default/spring-boot-84cd88dd7-5dwck to i-02d6699d4ada43805
  Normal  Pulled     90s   kubelet            Container image "602401143452.dkr.ecr.ap-northeast-1.amazonaws.com/eks/observability/adot-autoinstrumentation-java:v2.10.0" already present on machine
  Normal  Created    90s   kubelet            Created container: opentelemetry-auto-instrumentation-java
  Normal  Started    90s   kubelet            Started container opentelemetry-auto-instrumentation-java
  Normal  Pulling    89s   kubelet            Pulling image "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/spring-boot-sample-app:v3"
  Normal  Pulled     85s   kubelet            Successfully pulled image "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/spring-boot-sample-app:v3" in 4.7s (4.7s including waiting). Image size: 236889469 bytes.
  Normal  Created    85s   kubelet            Created container: spring-boot
  Normal  Started    85s   kubelet            Started container spring-boot

動作確認

Application Signals のサービスページでも確認してみます。
無事、サービスとして追加されていました。

service.png

Application Signals のセットアップ画面に行くと、コンソールから追加していないものの監視対象のサービスとして表示されていることを確認できます。

app5.png

SLO 管理を行うこともできました。

slo.png

トレース情報も確認できました。

trace.png

Java のランタイムメトリクスも見れます。

runtime.png

Observability アドオンをセットアップしているので、当然 Container Insights のダッシュボードも見れます。

container.png

最後に

EKS で Application Signals を有効化してみました。
以前 ECS で Application Signals を利用した時と比較すると、設定が非常に楽に感じました。
Container Insights のダッシュボードや、Observability アドオンに含まれた Fluent Bit も合わせて、初期設定の簡単さは目を見張るものがあります。
Prometheus + Grafana などの OSS 系や、Datadog などの SaaS 系も良いですが、AWS 純正サービスも充実してきているので、是非使っていきたいです。
後は Go とかに対応してくれたら良いなと思っています。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.