Fargate で Tomcat の Java/JMX を取得し CloudWatch に表示する

# やりたいこと

Fargate で Tomcat の Java/JMX を取得し CloudWatch メトリクスとして表示する。
また、Container Insights に表示する。


# 参考にしたサイト

https://aws.amazon.com/jp/blogs/mt/monitor-and-scale-your-amazon-ecs-on-aws-fargate-application-using-prometheus-metrics/

https://qiita.com/tayaaa/items/94f1baf5952bfa195ca5

https://qiita.com/SightSeekerTw/items/931834efbcff5b61eb0f


# 先にハマったところ

CloudWatch メトリクスまで表示させるのであれば、2 つ目の記事で良いが、Container Insights まで表示させるのであれば、1 つ目の AWS のブログの記事を参考にした方が良い。

CloudWatch エージェントの設定をパラメータストアに保存(CWAgentConfigSSMParameter)するのですが、この中で CloudWatch の名前空間を 2 つ目の記事では`"metric_namespace": "CWAgent","ECS/ContainerInsights",`にしている。この場合、CloudWatch メトリクスまでは表示されたが、Container Insights には表示されなかった。

1 つ目の AWS のブログでは`"metric_namespace"`を明示的にしてしていないため、名前空間には、`ECS/ContainerInsights/Prometheus`として出来上がる。これで Container Insights に表示された。

# Java/JMX の Prometheus メトリクス

CloudWatch エージェントにより収集される Java/JMX の Prometheus メトリクスは以下の通りです。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/ContainerInsights-Prometheus-metrics.html#ContainerInsights-Prometheus-metrics-jmx

# 手順

## Container Insights を有効にする

下記にサイトを参考に ECS クラスターで　Container Insights が有効になっていない場合は有効にしておきます。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/deploy-container-insights-ECS-cluster.html

## セキュリティグループ

セキュリティグループの受信ルールで、Prometheus メトリクスをプライベート IP でスクレイピングするために、CloudWatch エージェントへの Prometheus ポートを開く必要がある点に留意が必要です。

ここでは、`9404` 番ポートを開放しておきます。

## CWA の設定

下記のサイトにある**awsvpc ネットワークモードでの Fargate クラスターの CloudFormation スタックの作成** よりデプロイしますが、CloudFormation テンプレート(cwagent-ecs-prometheus-metric-for-awsvpc.yaml)内の `CWAgentConfigSSMParameter`　リソースを次項の「CloudWatch エージェント設定」の内容に置き換えてデプロイするだけです。

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

ここで、CloudWatch エージェント設定と Prometheus スクレイプ設定について説明しておきます。

Prometheus モニタリングを使用した CloudWatch エージェントは、Prometheus メトリクスをスクレイプするために 2 つの設定が必要です。

これらは、タスク定義のシークレットによって Parameter Store と統合されます。

|設定ファイル|タスク定義のシークレット|
|---|---|
|CloudWatch エージェント設定用|CW_CONFIG_CONTENT|
|Prometheus スクレイプ設定用|PROMETHEUS_CONFIG_CONTENT|

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

### CloudWatch エージェント設定

各フィールドの説明はドキュメントを参考にすれば良いので割愛します。

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

```json
        {
          "agent": {
            "debug": true
          },
          "logs": {
            "metrics_collected": {
              "prometheus": {
                "prometheus_config_path": "env:PROMETHEUS_CONFIG_CONTENT",
                "ecs_service_discovery": {
                  "sd_frequency": "1m",
                  "sd_result_file": "/tmp/cwagent_ecs_auto_sd.yaml",
                  "docker_label": {
                    "sd_port_label": "ECS_PROMETHEUS_EXPORTER_PORT",
                    "sd_job_name_label": "ECS_PROMETHEUS_JOB_NAME"
                  },
                  "task_definition_list": [
                    {
                      "sd_job_name": "bugbash-workload-java-ec2-awsvpc-task-def-sd",
                      "sd_metrics_ports": "9404;9406",
                      "sd_task_definition_arn_pattern": ".*:task-definition/tomcat*"
                    }
                  ]
                },
                "emf_processor": {
                  "metric_declaration_dedup": true,
                  "metric_declaration": [
                    {
                      "source_labels": ["Java_EMF_Metrics"],
                      "label_matcher": "^true$",
                      "dimensions": [["ClusterName","TaskDefinitionFamily"]],
                      "metric_selectors": [
                        "^jvm_threads_(current|daemon)$",
                        "^jvm_classes_loaded$",
                        "^java_lang_operatingsystem_(freephysicalmemorysize|totalphysicalmemorysize|freeswapspacesize|totalswapspacesize|systemcpuload|processcpuload|availableprocessors|openfiledescriptorcount)$",
                        "^catalina_manager_(rejectedsessions|activesessions)$",
                        "^jvm_gc_collection_seconds_(count|sum)$",
                        "^catalina_globalrequestprocessor_(bytesreceived|bytessent|requestcount|errorcount|processingtime)$"
                      ]
                    },
                    {
                      "source_labels": ["Java_EMF_Metrics"],
                      "label_matcher": "^true$",
                      "dimensions": [["ClusterName","TaskDefinitionFamily","pool"]],
                      "metric_selectors": [
                        "^jvm_memory_pool_bytes_used$"
                      ]
                    },
                    {
                      "source_labels": ["Java_EMF_Metrics"],
                      "label_matcher": "^true$",
                      "dimensions": [["ClusterName","TaskDefinitionFamily","area"]],
                      "metric_selectors": [
                        "^jvm_memory_bytes_used$"
                      ]
                    }
                  ]
                }
              }
            },
            "force_flush_interval": 5
          }
        }
```

### Prometheus スクレイプ設定

こちらも各フィールドの説明はドキュメントを参考にすれば良いので割愛します。

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

```yaml
global:
  scrape_interval: 1m
  scrape_timeout: 10s
scrape_configs:
  - job_name: cwagent-ecs-file-sd-config
    sample_limit: 10000
    file_sd_configs:
      - files: [ "/tmp/cwagent_ecs_auto_sd.yaml" ]
```

### タスクロール

タスクロールに必要な権限の説明については以下の通りです。

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

ECS Prometheus サービス検出を有効にするとき、CloudWatch エージェントは定期的に ECS および EC2 フロントエンドに対して次の API コールを行い、ターゲット ECS クラスターで実行中の ECS タスクのメタデータを取得するために下記の`ECSServiceDiscoveryInlinePolicy`が必要ということになります。

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

- [CloudWatchAgentServerPolicy](https://docs.aws.amazon.com/ja_jp/aws-managed-policy/latest/reference/CloudWatchAgentServerPolicy.html)
- ECSServiceDiscoveryInlinePolicy

```json:ECSServiceDiscoveryInlinePolicy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Condition": {
                "ArnEquals": {
                    "ecs:cluster": "arn:aws:ecs:ap-northeast-1:123456789012:cluster/tomcat-ecs-cluster"
                }
            },
            "Action": [
                "ecs:DescribeTasks",
                "ecs:ListTasks",
                "ecs:DescribeContainerInstances",
                "ecs:DescribeServices",
                "ecs:ListServices"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "ec2:DescribeInstances",
                "ecs:DescribeTaskDefinition"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}
```

### タスク実行ロール

タスク実行ロールに必要な権限の説明については以下の通りです。

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

- [AmazonECSTaskExecutionRolePolicy](https://docs.aws.amazon.com/ja_jp/aws-managed-policy/latest/reference/AmazonECSTaskExecutionRolePolicy.html)
- [CloudWatchAgentServerPolicy](https://docs.aws.amazon.com/ja_jp/aws-managed-policy/latest/reference/CloudWatchAgentServerPolicy.html)
- ECSSSMInlinePolicy

```json:ECSSSMInlinePolicy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "ssm:GetParameters"
            ],
            "Resource": "arn:aws:ssm:*:*:parameter/AmazonCloudWatch-*",
            "Effect": "Allow"
        }
    ]
}
```

### タスク定義

ここでのポイントは先に説明した通り、Prometheus スクレイプ設定と CloudWatch エージェント設定を secrets として渡している点です。

```json
{
    "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:123456789012:task-definition/cwagent-prometheus-tomcat-ecs-cluster-FARGATE-awsvpc:1",
    "containerDefinitions": [
        {
            "name": "cloudwatch-agent-prometheus",
            "image": "public.ecr.aws/cloudwatch-agent/cloudwatch-agent:1.300039.0b612",
            "cpu": 0,
            "links": [],
            "portMappings": [],
            "essential": true,
            "entryPoint": [],
            "command": [],
            "environment": [],
            "environmentFiles": [],
            "mountPoints": [],
            "volumesFrom": [],
            "secrets": [
                {
                    "name": "PROMETHEUS_CONFIG_CONTENT",
                    "valueFrom": "AmazonCloudWatch-PrometheusConfigName-tomcat-ecs-cluster-FARGATE-awsvpc"
                },
                {
                    "name": "CW_CONFIG_CONTENT",
                    "valueFrom": "AmazonCloudWatch-CWAgentConfig-tomcat-ecs-cluster-FARGATE-awsvpc"
                }
            ],
            "dnsServers": [],
            "dnsSearchDomains": [],
            "extraHosts": [],
            "dockerSecurityOptions": [],
            "dockerLabels": {},
            "ulimits": [],
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-create-group": "True",
                    "awslogs-group": "/ecs/ecs-cwagent-prometheus",
                    "awslogs-region": "ap-northeast-1",
                    "awslogs-stream-prefix": "ecs-FARGATE-awsvpc"
                },
                "secretOptions": []
            },
            "systemControls": [],
            "credentialSpecs": []
        }
    ],
    "family": "cwagent-prometheus-tomcat-ecs-cluster-FARGATE-awsvpc",
    "taskRoleArn": "arn:aws:iam::123456789012:role/tomcat-ecs-task-role",
    "executionRoleArn": "arn:aws:iam::123456789012:role/tomcat-ecs-execution-role",
    "networkMode": "awsvpc",
    "revision": 8,
    "volumes": [],
    "status": "ACTIVE",
    "requiresAttributes": [
        {
            "name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
        },
        {
            "name": "ecs.capability.execution-role-awslogs"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.17"
        },
        {
            "name": "com.amazonaws.ecs.capability.task-iam-role"
        },
        {
            "name": "ecs.capability.secrets.ssm.environment-variables"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
        },
        {
            "name": "ecs.capability.task-eni"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.29"
        }
    ],
    "placementConstraints": [],
    "compatibilities": [
        "EC2",
        "FARGATE"
    ],
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "512",
    "memory": "1024",
    "registeredAt": "2024-05-21T01:34:22.288Z",
    "registeredBy": "arn:aws:iam::123456789012:user/vscode",
    "tags": []
}
```

### サービス

サービスでは特別な設定はありませんので、次の通りタスクが起動したことを確認しておきます。
![スクリーンショット 2024-05-22 14.05.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/61e13dbf-dd71-bbaa-2820-d6fa853d174d.png)

## tomcat の設定

基本的に下記のページを参考に作成していきます。

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

### jmx_prometheus_javaagent

[prometheus/jmx_exporter](https://github.com/prometheus/jmx_exporter) から最新バージョンのもの（投稿時点では、jmx_prometheus_javaagent-0.20.0.jar）をダウンロードします。

### config.yaml

`config.yaml`は JMX Exporter 設定ファイルです。

```yaml:config.yaml
lowercaseOutputName: true
lowercaseOutputLabelNames: true

rules:
- pattern: 'java.lang<type=OperatingSystem><>(FreePhysicalMemorySize|TotalPhysicalMemorySize|FreeSwapSpaceSize|TotalSwapSpaceSize|SystemCpuLoad|ProcessCpuLoad|OpenFileDescriptorCount|AvailableProcessors)'
  name: java_lang_OperatingSystem_$1
  type: GAUGE

- pattern: 'java.lang<type=Threading><>(TotalStartedThreadCount|ThreadCount)'
  name: java_lang_threading_$1
  type: GAUGE

- pattern: 'Catalina<type=GlobalRequestProcessor, name=\"(\w+-\w+)-(\d+)\"><>(\w+)'
  name: catalina_globalrequestprocessor_$3_total
  labels:
    port: "$2"
    protocol: "$1"
  help: Catalina global $3
  type: COUNTER

- pattern: 'Catalina<j2eeType=Servlet, WebModule=//([-a-zA-Z0-9+&@#/%?=~_|!:.,;]*[-a-zA-Z0-9+&@#/%=~_|]), name=([-a-zA-Z0-9+/$%~_-|!.]*), J2EEApplication=none, J2EEServer=none><>(requestCount|maxTime|processingTime|errorCount)'
  name: catalina_servlet_$3_total
  labels:
    module: "$1"
    servlet: "$2"
  help: Catalina servlet $3 total
  type: COUNTER

- pattern: 'Catalina<type=ThreadPool, name="(\w+-\w+)-(\d+)"><>(currentThreadCount|currentThreadsBusy|keepAliveCount|pollerThreadCount|connectionCount)'
  name: catalina_threadpool_$3
  labels:
    port: "$2"
    protocol: "$1"
  help: Catalina threadpool $3
  type: GAUGE

- pattern: 'Catalina<type=Manager, host=([-a-zA-Z0-9+&@#/%?=~_|!:.,;]*[-a-zA-Z0-9+&@#/%=~_|]), context=([-a-zA-Z0-9+/$%~_-|!.]*)><>(processingTime|sessionCounter|rejectedSessions|expiredSessions)'
  name: catalina_session_$3_total
  labels:
    context: "$2"
    host: "$1"
  help: Catalina session $3 total
  type: COUNTER

- pattern: ".*"
```

### setenv.sh

Tomcat とともに JMX Exporter を起動し、localhost のポート `9404` で Prometheus メトリクスを公開する Tomcat 起動スクリプトです。また、config.yaml ファイルパスを JMX Exporter に提供します。

```sh:setenv.sh
export JAVA_OPTS="-javaagent:/opt/jmx_exporter/jmx_prometheus_javaagent-0.20.0.jar=9404:/opt/jmx_exporter/config.yaml $JAVA_OPTS"
```

### sample.war

適当に以下のアプリケーションを利用しました。

https://tomcat.apache.org/tomcat-7.0-doc/appdev/sample/

### Dockerfile

ここまでの内容を Dockerfile に記述します。

```Dockerfile:Dockerfile
FROM tomcat:9.0-jdk8-openjdk 

RUN mkdir -p /opt/jmx_exporter

COPY ./jmx_prometheus_javaagent-0.20.0.jar /opt/jmx_exporter
COPY ./config.yaml /opt/jmx_exporter
COPY ./setenv.sh /usr/local/tomcat/bin
COPY sample.war /usr/local/tomcat/webapps/

RUN chmod  o+x /usr/local/tomcat/bin/setenv.sh

ENTRYPOINT ["catalina.sh", "run"]
```

### Docker イメージの作成

ここまでの手順で以下の通り必要なファイルが出来上がっていると思います。

.
├── Dockerfile
├── config.yaml
├── jmx_prometheus_javaagent-0.20.0.jar
├── sample.war
└── setenv.sh

Build 後、CodeCommit に Push します。

```
docker build --platform linux/x86_64 -t tomcat . 
docker tag tomcat:latest 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/tomcat:latest
docker push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/tomcat:latest
```

### タスク定義

ここでのポイントは、dockerLabels を指定する点です。
`ECS_PROMETHEUS_EXPORTER_PORT` ラベル付きのコンテナは、Prometheus スクレイピング用に指定されたコンテナポートに基づいて自動検出されます。
`Java_EMF_Metrics=“true”`ラベル付きのコンテナの場合、埋め込みメトリクス形式が生成されます。

|Key|Value|
|---|---|
|ECS_PROMETHEUS_EXPORTER_PORT|９４０４|
|Java_EMF_Metrics|true|

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

```json
{
    "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:123456789012:task-definition/tomcat-tf:8",
    "containerDefinitions": [
        {
            "name": "tomcat",
            "image": "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/tomcat:latest",
            "cpu": 0,
            "portMappings": [
                {
                    "name": "tomcat-8080-tcp",
                    "containerPort": 8080,
                    "hostPort": 8080,
                    "protocol": "tcp",
                    "appProtocol": "http"
                },
                {
                    "name": "tomcat-9404-tcp",
                    "containerPort": 9404,
                    "hostPort": 9404,
                    "protocol": "tcp"
                }
            ],
            "essential": true,
            "environment": [],
            "environmentFiles": [],
            "mountPoints": [],
            "volumesFrom": [],
            "dockerLabels": {
                "ECS_PROMETHEUS_EXPORTER_PORT": "9404",
                "Java_EMF_Metrics": "true"
            },
            "ulimits": [],
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-create-group": "true",
                    "awslogs-group": "/ecs/tomcat-tf",
                    "awslogs-region": "ap-northeast-1",
                    "awslogs-stream-prefix": "ecs"
                },
                "secretOptions": []
            },
            "systemControls": []
        }
    ],
    "family": "tomcat-tf",
    "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
    "networkMode": "awsvpc",
    "revision": 8,
    "volumes": [],
    "status": "ACTIVE",
    "requiresAttributes": [
        {
            "name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
        },
        {
            "name": "ecs.capability.execution-role-awslogs"
        },
        {
            "name": "com.amazonaws.ecs.capability.ecr-auth"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
        },
        {
            "name": "ecs.capability.execution-role-ecr-pull"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
        },
        {
            "name": "ecs.capability.task-eni"
        },
        {
            "name": "com.amazonaws.ecs.capability.docker-remote-api.1.29"
        }
    ],
    "placementConstraints": [],
    "compatibilities": [
        "EC2",
        "FARGATE"
    ],
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "1024",
    "memory": "3072",
    "runtimePlatform": {
        "cpuArchitecture": "X86_64",
        "operatingSystemFamily": "LINUX"
    },
    "registeredAt": "2024-05-21T13:37:02.633Z",
    "registeredBy": "arn:aws:iam::123456789012:user/leomaro7",
    "tags": []
}
```

### サービス

サービスでは特別な設定はありませんので、次の通りタスクが起動したことを確認しておきます。

![スクリーンショット 2024-05-22 14.21.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/1cb20c02-498e-8be9-bc2f-de4adbd517ed.png)

## 動作確認

### JMX のメトリクスが出力されることの確認

次の通り、`9404` 番ポートにアクセスし、JMX のメトリクスが出力されることを確認しておきます。

```
curl X.X.X.X:9404/metrics

# HELP jmx_config_reload_success_total Number of times configuration have successfully been reloaded.
# TYPE jmx_config_reload_success_total counter
jmx_config_reload_success_total 0.0
# HELP jmx_exporter_build_info A metric with a constant '1' value labeled with the version of the JMX exporter.
# TYPE jmx_exporter_build_info gauge
jmx_exporter_build_info{version="0.20.0",name="jmx_prometheus_javaagent",} 1.0
# HELP jvm_buffer_pool_used_bytes Used bytes of a given JVM buffer pool.
# TYPE jvm_buffer_pool_used_bytes gauge
jvm_buffer_pool_used_bytes{pool="direct",} 98303.0
jvm_buffer_pool_used_bytes{pool="mapped",} 0.0
```

### アプリケーションの動作確認

一応、`8080` 番ポートにアクセスし、アプリケーションの動作も確認しておきます。

```
curl X.X.X.X:8080/sample
```

### CloudWatch ログ

`/aws/ecs/containerinsights/cluster-name/Prometheus`に記録されたことが確認できました。
![スクリーンショット 2024-05-22 15.48.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/619b3a6e-91d5-2c11-1e7c-a2da1216e0bf.png)

### CloudWatch メトリクス 

名前空間`ECS/ContainerInsights/Prometheus`に表示されたことを確認できました。
![スクリーンショット 2024-05-22 15.46.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/41f746d5-df62-76c1-fab1-cae7d24ea6a9.png)

CloudWatch Logs に送信された Prometheus メトリクスが表示されない場合などは以下のサイトを参考にトラブルシュートすることをお勧めします。

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

### Container Insights

Container Insights　に表示されたことも確認できました。
![スクリーンショット 2024-05-22 15.54.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/04beb899-2f26-4b9f-8241-d12995f58446.png)