📌

ECS で Java/JMX ワークロードの Prometheus メトリクスを CloudWatch エージェントを使用して収取するデモ

2024/05/30に公開

この記事の概要

ECS で Java/JMX ワークロードの Prometheus メトリクスを CloudWatch エージェントを使用して収取するデモを、公式 docs にあるサンプルをもとにやってみます。

CloudWatch Agent が Prometheus メトリクスを収集するコレクターとなりエクスポートされた Prometheus メトリクスを読み込み、別途 EMF(embedded metric format) で CloudWatch ログ経由で CloudWatch メトリクスに出力することが可能です。

イメージはこんな感じです。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/ContainerInsights-Prometheus-Setup-ECS.html

Amazon ECS クラスターから Prometheus メトリクスを収集するには、CloudWatch エージェントをコレクターとして使用するか、AWS Distro for OpenTelemetry コレクターを使用できます。
...
以下のセクションでは、CloudWatch エージェントをコレクターとして使用して Prometheus メトリクスを取得する方法について説明します。Amazon ECS を実行しているクラスターに Prometheus モニターリングを使用して CloudWatch エージェントをインストールし、オプションで追加のターゲットをスクレイプするようにエージェントを設定することができます。また、これらのセクションでは、Prometheus モニターリングでのテストに使用するサンプルワークロードを設定するためのオプションのチュートリアルも提供します。

Amazon ECS の Container Insights は、Prometheus メトリクスの次の起動タイプとネットワークモードの組み合わせをサポートしています。

Amazon ECS 起動タイプ サポートされているネットワークモード
EC2 (Linux) ブリッジ、ホストおよび awsvpc
Fargate awsvpc

デモ

Javaサンプルアプリを用意して、Prometheusメトリクスを取得していきます。
なお、今回検証で Java が必要になったために実施していますが、Java は全くわかりません。

1. CWエージェントを ECS クラスターにインストール

正確には、CWエージェントタスクを、特定の ECS クラスターで起動します。

こちらにある方法をもとにしています。
EC2起動タイプ- bridge モードで試していきます。
https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/ContainerInsights-Prometheus-install-ECS.html

タスクロールとタスク実行ロール

  • ECS タスクロール – CloudWatch エージェントコンテナはこのロールを使用します。これには、CloudWatchAgentServerPolicy ポリシーと、次の読み取り専用のアクセス許可を含むカスタマー管理ポリシーが含まれている必要があります。

    ec2:DescribeInstances
    ecs:ListTasks
    ecs:ListServices
    ecs:DescribeContainerInstances
    ecs:DescribeServices
    ecs:DescribeTasks
    ecs:DescribeTaskDefinition

  • ECS タスク実行ロール – タスク実行ロールに [AmazonSSMReadOnlyAccess]、[AmazonECSTaskExecutionRolePolicy]、[CloudWatchAgentServerPolicy] ポリシーがアタッチされていることを確認します。

CWエージェントタスクの起動

こちらにサンプルとなる CFn テンプレートをデプロイするコマンドが記載されています。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/ContainerInsights-Prometheus-install-ECS.html

$ export AWS_PROFILE=default
$ export AWS_DEFAULT_REGION=ap-northeast-1
$ export ECS_CLUSTER_NAME=default
$ export ECS_NETWORK_MODE=bridge
$ export CREATE_IAM_ROLES=False
$ export ECS_CLUSTER_SECURITY_GROUP=sg-xxxxxxx
$ export ECS_TASK_ROLE_NAME=ecsTaskRole
$ export ECS_EXECUTION_ROLE_NAME=ecsTaskExecutionRole

$ curl -O https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/ecs-task-definition-templates/deployment-mode/replica-service/cwagent-prometheus/cloudformation-quickstart/cwagent-ecs-prometheus-metric-for-bridge-host.yaml

$ aws cloudformation create-stack --stack-name CWAgent-Prometheus-ECS-${ECS_CLUSTER_NAME}-EC2-${ECS_NETWORK_MODE} \
    --template-body file://cwagent-ecs-prometheus-metric-for-bridge-host.yaml \
    --parameters ParameterKey=ECSClusterName,ParameterValue=${ECS_CLUSTER_NAME} \
                 ParameterKey=CreateIAMRoles,ParameterValue=${CREATE_IAM_ROLES} \
                 ParameterKey=ECSNetworkMode,ParameterValue=${ECS_NETWORK_MODE} \
                 ParameterKey=TaskRoleName,ParameterValue=${ECS_TASK_ROLE_NAME} \
                 ParameterKey=ExecutionRoleName,ParameterValue=${ECS_EXECUTION_ROLE_NAME} \
    --capabilities CAPABILITY_NAMED_IAM \
    --region ${AWS_DEFAULT_REGION} \
    --profile ${AWS_PROFILE}

CWエージェントタスクが起動しました。

2.モニタリングする Java アプリの用意

Java アプリが必要ですが、作ったこともありません。
Cloud9環境のサンプルを試します。
https://docs.aws.amazon.com/ja_jp/cloud9/latest/user-guide/sample-java.html

  • Cloud9 で [Amazonlinux2] を選択します。
    インスタンスサイズでt3.small (2 GiB RAM + 2 vCPU)を選択
    最初、1 GiB RAM のみしかない状態でしたがメモリ不足でビルド失敗したため、大きいものを選択しておきます。

  • ^ の公式docsのままやっていくと Java のバージョンの問題でうまくいかなかったところもありました。その時のエラーを生成AIに聞きながら、とりあえず動くように進めています。

以下、Cloud9環境下で実施してきます。

$ java -version
openjdk version "17.0.11" 2024-04-16 LTS
OpenJDK Runtime Environment Corretto-17.0.11.9.1 (build 17.0.11+9-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.11.9.1 (build 17.0.11+9-LTS, mixed mode, sharing)

$ javac -version
javac 1.7.0_321

$ sudo yum -y update

// Maven を使用して設定する
$ sudo wget http://repos.fedorapeople.org/repos/dchen/apache-maven/epel-apache-maven.repo -O /etc/yum.repos.d/epel-apache-maven.repo
$ sudo sed -i s/\$releasever/6/g /etc/yum.repos.d/epel-apache-maven.repo
$ sudo yum install -y apache-maven

$ mvn -version
Apache Maven 3.5.2 (138edd61fd100ec658bfa2d307c43b76940a5d7d; 2017-10-18T07:58:13Z)
Maven home: /usr/share/apache-maven
Java version: 17.0.11, vendor: Amazon.com Inc.
Java home: /usr/lib/jvm/java-17-amazon-corretto.x86_64
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "5.10.216-204.855.amzn2.x86_64", arch: "amd64", family: "unix"

$ mvn archetype:generate -DgroupId=com.mycompany.app -DartifactId=my-app -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

// 公式docs にあるpom.xmlではうまくいかなかったので以下に修正
$ cat pom.xml
(この章の最後に記載)

// 30分間、30s ごとに現在時刻を出力するアプリに変更
$ cat my-app/src/main/java/com/mycompany/app/App.java 
(この章の最後に記載)

$ cd my-app
$ mvn package

// アプリを実行してみます
$ java -cp target/my-app-1.0-SNAPSHOT-jar-with-dependencies.jar com.mycompany.app.App
Current time: 2024-05-29 01:29:13
Current time: 2024-05-29 01:29:43
Current time: 2024-05-29 01:30:13
...
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.mycompany.app</groupId>
  <artifactId>my-app</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <source>1.8</source> <!-- ここでJavaのバージョンを指定 -->
          <target>1.8</target> <!-- ここでJavaのバージョンを指定 -->
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>3.6.0</version>
        <configuration>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
          <archive>
            <manifest>
              <mainClass>com.mycompany.app.App</mainClass>
            </manifest>
          </archive>
        </configuration>
        <executions>
          <execution>
            <phase>package</phase>
              <goals>
                <goal>single</goal>
              </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>
my-app/src/main/java/com/mycompany/app/App.java
// 30分間、30s ごとに現在時刻を出力します
package com.mycompany.app;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class App {
    public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
        Runnable task = () -> {
            LocalDateTime now = LocalDateTime.now();
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            String formattedTime = now.format(formatter);
            System.out.println("Current time: " + formattedTime);
        };

        executor.scheduleAtFixedRate(task, 0, 30, TimeUnit.SECONDS);

        try {
            Thread.sleep(30 * 60 * 1000); // 30分間実行
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            executor.shutdown();
        }
    }
}

3. JMX Exporter をエージェントとして組み込んだアプリケーションのコンテナイメージを作成

$ tree
.
├── config.yaml
├── Dockerfile
├── jmx_prometheus_javaagent-0.20.0.jar
├── my-app-1.0-SNAPSHOT.jar
└── start_exporter_example.sh

$ cat start_exporter_example.sh 
java -javaagent:/opt/jmx_exporter/jmx_prometheus_javaagent-0.20.0.jar=9404:/opt/jmx_exporter/config.yaml -cp  /opt/jmx_exporter/my-app-1.0-SNAPSHOT.jar com.mycompany.app.App
Dockerfile
FROM openjdk:8-jre-alpine

RUN mkdir -p /opt/jmx_exporter

COPY ./jmx_prometheus_javaagent-0.20.0.jar /opt/jmx_exporter
COPY ./my-app-1.0-SNAPSHOT.jar /opt/jmx_exporter
COPY ./start_exporter_example.sh /opt/jmx_exporter
COPY ./config.yaml /opt/jmx_exporter

RUN chmod -R o+x /opt/jmx_exporter
RUN apk add curl

ENTRYPOINT exec /opt/jmx_exporter/start_exporter_example.sh
  • config.yaml はこちらにあるのをそのまま使用します。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/ContainerInsights-Prometheus-Sample-Workloads-javajmx.html#ContainerInsights-Prometheus-Sample-Workloads-javajmx-jar

  • config.yaml のサンプルとしてはこちらに複数あります。

https://github.com/prometheus/jmx_exporter/tree/main/example_configs

  • JMX Exporter エージェントファイルはこちらからダウンロード

https://github.com/prometheus/jmx_exporter/releasessu

4. Java アプリを起動

タスク定義

{
    "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:xxxxxxxxx:task-definition/workload-java-ec2-bridge:1",
    "containerDefinitions": [
        {
            "name": "<コンテナ名>",
            "image": "<コンテナイメージURI>",
            "cpu": 0,
            "portMappings": [
                {
                    "name": "port-9404-tcp",
                    "containerPort": 9404,
                    "hostPort": 0,
                    "protocol": "tcp"
                }
            ],
            ...
            "dockerLabels": {
                "ECS_PROMETHEUS_EXPORTER_PORT": "9404",
                "Java_EMF_Metrics": "true"
            },
            "systemControls": []
        }
    ],
    "family": "workload-java-ec2-bridge",
    "taskRoleArn": "arn:aws:iam::xxxxxxx:role/ecsTaskRole",
    "executionRoleArn": "arn:aws:iam::xxxxxxx:role/ecsTaskExecutionRole",
    "networkMode": "bridge",
    ...
    "requiresCompatibilities": [
        "EC2"
    ],
...

5. Container Insights 確認

最終的にこのようなイメージになります。
JMX Exporter のポート:9404 を開いて、このポートから Prometheus メトリクスを公開します。

CloudWatch コンソールから、Container Insights の画面を確認してみます。

6. メトリクス確認

EC2 起動タイプなので、EC2へアクセスして公開されているメトリクスを確認してみます。

[root@ip-172-31-141-209 ~]# docker ps
CONTAINER ID   IMAGE                                                                COMMAND                  CREATED          STATUS                    PORTS                                         NAMES
996d5881ef3e   225703910872.dkr.ecr.ap-northeast-1.amazonaws.com/java-sample:test   "/bin/sh -c 'exec /o…"   27 minutes ago   Up 27 minutes             0.0.0.0:32768->9404/tcp, :::32768->9404/tcp   ecs-workload-java-ec2-bridge-2-tomcat-prometheus-workload-java-ec2-bridge-dynamic-port-d2dee18ae283acab4500
0d151bc51fd5   public.ecr.aws/cloudwatch-agent/cloudwatch-agent:1.300039.0b612      "/opt/aws/amazon-clo…"   27 minutes ago   Up 27 minutes                                                           ecs-cwagent-prometheus-default-EC2-bridge-1-cloudwatch-agent-prometheus-b69580d0fcd0b8e79e01
554f558f9f2a   amazon/amazon-ecs-agent:latest                                       "/agent"                 28 minutes ago   Up 28 minutes (healthy)                                                 ecs-agent

[root@ip-172-31-141-209 ~]# docker inspect 996d5881ef3e | grep -i ipaddress
            "SecondaryIPAddresses": null,
            "IPAddress": "172.17.0.3",
                    "IPAddress": "172.17.0.3",

[root@ip-172-31-141-209 ~]# curl 172.17.0.3:9404/metrics
# HELP jvm_info VM version info
# TYPE jvm_info gauge
jvm_info{runtime="OpenJDK Runtime Environment",vendor="IcedTea",version="1.8.0_212-b04",} 1.0
# HELP jvm_threads_current Current thread count of a JVM
# TYPE jvm_threads_current gauge
jvm_threads_current 8.0
....
# HELP jvm_memory_pool_allocated_bytes_created Total bytes allocated in a given JVM memory pool. Only updated after GC, not continuously.
# TYPE jvm_memory_pool_allocated_bytes_created gauge
jvm_memory_pool_allocated_bytes_created{pool="Eden Space",} 1.716947643014E9
jvm_memory_pool_allocated_bytes_created{pool="Code Cache",} 1.716947643107E9
jvm_memory_pool_allocated_bytes_created{pool="Compressed Class Space",} 1.716947643107E9
jvm_memory_pool_allocated_bytes_created{pool="Metaspace",} 1.716947643107E9
jvm_memory_pool_allocated_bytes_created{pool="Tenured Gen",} 1.716947942906E9
jvm_memory_pool_allocated_bytes_created{pool="Survivor Space",} 1.716947643107E9

まだよくわかっていないこと・今後の検証

Q:JMX Exporter により取得するメトリクスはどこでいじれる?
A:config.yaml で指定するのかという認識。

Q:エージェントとHTTPサーバータイプがあるらしいけど。
A:githubにはエージェント型を強くお勧めしているようです。

https://github.com/prometheus/jmx_exporter/tree/release-0.20.0

Q:メトリクスを収集するCWエージェント側での設定は?
A:CWエージェントの設定ファイルは、今回でいうと「cwagent-ecs-prometheus-metric-for-bridge-host.yaml」の AWS::SSM::Parameter で定義している。
なのでそこで定義されている設定をいじることで、追加のターゲットをスクレイピングできる。
CWエージェントコンテナの PROMETHEUS_CONFIG_CONTENT, CW_CONFIG_CONTENT の2つがそれぞれ Prometheus スクレイプ設定用, CloudWatch エージェント設定用 のシークレットになっている。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/ContainerInsights-Prometheus-Setup-configure-ECS.html

Discussion