【ECS】Fargate上で動かすTomcatコンテナのJMX情報を取得する

# はじめに
Fargate上で動かすTomcatコンテナ内のJava Management Extensions (以下 JMX)の情報を取得するために、jmx_exporter + CloudWatchAgent の構成で検証しました。  
jmx_exporter( https://github.com/prometheus/jmx_exporter )とは、Prometheus向けのJavaのエクスポーターで、JMXの情報をPrometheus用に出力してくれます。この情報をCloudWatchAgent(以下CWA)で取得し、CloudWatchLogsに出力します。また、CloudWatchLogsに出力された情報を基にカスタムメトリクスを作成することで、JMXの情報の可視化や監視を行うことが可能になります。  
jmx_exporterはPrometheus用のエクスポーターなので、Prometheusを構築する必要があるのでは？と思いましたが、CWAがPrometheus用の情報も取り扱えるので、不要とのことです。嬉しいですね。

# 構成図
今回作成した構成のイメージは以下のようになっています。  

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3571776/c559a73d-9858-a3e2-1dd5-d0ab4f8e2b87.png" width=800>


大まかな流れとしては、  
1. CWAからECSに対してクラスタ内で起動しているサービスを取得する。
2. CWAコンテナからTomcatコンテナのPrometheusエンドポイント（に指定したポート）に対してコールを行い、JMXメトリクスを収集する。
3. CWAでCloudWatchLogsに収集したログを出力する。

となっています。

# 手順
## Tomcatコンテナ
### 1. jmx_exporterの構築
#### Dockerfile
Tomcatを動かすコンテナのDockerfileに、jmx_exporterをインストールするように記述します。  
jmx_exporterは https://github.com/prometheus/jmx_exporter から取得します。

```Docker: Dockerfile
RUN mkdir -p /opt/jmx_exporter
COPY ./jmx_prometheus_javaagent-0.20.0.jar /opt/jmx_exporter
COPY ./config.yaml /opt/jmx_exporter
```

#### config.yaml
jmx_exporterの設定ファイルである`config.yaml`ですが、今回は特に設定していません。

```yaml: config.yaml
rules:
- pattern: ".*"
```

詳細を知りたい方は公式ドキュメントを参照してください。  
https://github.com/prometheus/jmx_exporter?tab=readme-ov-file#configuration   
サンプルケースも多く用意されています。  
https://github.com/prometheus/jmx_exporter/tree/main/example_configs

#### setenv.sh
Tomcatの`setenv.sh`にjmx_exporterをJavaエージェントとして動かすように設定します。
```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"
```
`jmx_prometheus_javaagent-0.20.0.jar=9404:`の部分でどのポートをPrometheusエンドポイントとするかを指定しています。コンテナを立ち上げ、localhost:9404/metricsにアクセスすると、JMXのメトリクスが出力されることが確認できます。

```sh
$ curl localhost:9404/metrics

# HELP jvm_memory_objects_pending_finalization The number of objects waiting in the finalizer queue.
# TYPE jvm_memory_objects_pending_finalization gauge
jvm_memory_objects_pending_finalization 0.0
# HELP jvm_memory_bytes_used Used bytes of a given JVM memory area.
# TYPE jvm_memory_bytes_used gauge
jvm_memory_bytes_used{area="heap",} 3.1449648E7
jvm_memory_bytes_used{area="nonheap",} 3.1373688E7
```

### 2. タスク定義の作成
Prometheusエンドポイントに指定したポートにアクセスできるように、ポートマッピングで9404を開放しておきます。


## CWAコンテナ
### 1. コンテナイメージの作成
CWAのコンテナイメージはAWS公式のパブリックイメージがあるので、それを使えばOKです。  
ただし今回はコンテナをプライベートサブネットで立ち上げるので、あらかじめプライベートリポジトリにプッシュしました。

### 2. 環境変数の作成
CWAのタスク定義で渡す環境変数を用意します。  
これらの環境変数はAWS Systems Manager パラメータストア(以下、SSMパラメータストア)に保存します。

#### PROMETHEUS_CONFIG_CONTENT
Prometheus用の環境変数である`PROMETHEUS_CONFIG_CONTENT`を以下のようにYAML形式で定義します。
```yaml
global:
  scrape_interval: 1m # エンドポイントをスクレイピングする間隔
  scrape_timeout: 10s # スクレイピングするときのタイムアウト設定
scrape_configs:
  - job_name: tomcat
    sample_limit: 10000 # 収集するメトリクスの最大数
    file_sd_configs:
      - files: ["/tmp/cwagent_ecs_auto_sd.yaml"]
```
スクレイピング(Scrayping)とは、特定の情報だけを抽出・取集する手法のことです。  
`files: ["/tmp/cwagent_ecs_auto_sd.yaml"]`は、CWAがどのターゲットをスクレイピングするかの情報が記述されるファイルなので、特に変更する必要はありません。


#### CWAGENT_CONFIG
CWA用の環境変数である`CWAGENT_CONFIG`を以下のようにJSON形式で記述します。
```json
{
    "agent": {
        "debug": false
    },
    "logs": {
        "metrics_collected": {
            "prometheus": {
                "log_group_name": "/ecs/cloudwatch-agent",
                "prometheus_config_path": "env:PROMETHEUS_CONFIG_CONTENT",
                "ecs_service_discovery": {
                    "sd_frequency": "1m",
                    "sd_result_file": "/tmp/cwagent_ecs_auto_sd.yaml",
                    "service_name_list_for_tasks": [
                        {
                            "sd_metrics_ports": "9404",
                            "sd_service_name_pattern": ".*",
                            "sd_metrics_path": "/metrics"
                        }
                    ]
                },
                "emf_processor": {
                    "metric_namespace": "CWAgent",
                    "metric_declaration": [
                        {
                            "source_labels": ["container_name"],
                            "label_matcher": "^tomcat$",
                            "dimensions": [["ClusterName", "TaskId", "area"]],
                            "metric_selectors": [
                                "^jvm_memory_bytes_used$",
                                "^jvm_memory_bytes_committed$"
                            ]
                        }
                    ]
                }
            }
        },
        "force_flush_interval": 5
    }
}
```
設定したパラメータについて私なりの理解で解説します。
- `log_group_name` : JMXメトリクスのログを出力する、ロググループを指定。
- `prometheus_config_path` : Prometheusのconfigを指定したパスから読み込む。
- `ecs_service_discovery` : CWAがクラスタ内でスクレイピングを行うECSサービスを探すための設定。
    - `sd_frequency` : ECSにサービス一覧を取得しにいく間隔。
    - `sd_result_file` : CWAがどのターゲットをスクレイピングするかの情報が記述されるファイル。
    - `service_name_list_for_tasks` : メトリクスを取得するサービス/タスクを指定する。
        - `sd_metrics_ports` : Prometheusエンドポイントに指定したポート。
        - `sd_service_name_pattern` : 正規表現でスクレピングを行うサービスのパターンを指定。
        - `sd_metrics_path` : メトリクスを取得する際にアクセスするパスを指定。今回だと`private_ip:9404/metrics`となる。
- `emf_processor` : CloudWatchメトリクスにカスタムメトリクスとして出力する設定。
    - `metric_namespace` : カスタムメトリクスの名前。
    - `metric_declaration` : どのJMXメトリクスをCloudWatchのカスタムメトリクスにとするかの設定。
        - `source_labels` : ログを絞り込むためのラベルを指定。(`container_name`や`TaskDefinitionFamily`などを指定可能)
        - `label_matcher` : `source_labels`の値が、一致するものだけをCloudWatchメトリクスに飛ばす。今回はコンテナ名がTomcatであるものだけに絞り込む。
        - `dimensions` : カスタムメトリクスのディメンジョンを指定する。
        - `metric_selectors` : カスタムメトリクスとして取り込む、JMXメトリクスを指定する。
- `force_flush_interval` : ログがサーバーに送信されるまでにメモリバッファ内に残留する最大時間を指定。

今回はクラスタ内のECS サービス検出の設定である`ecs_service_discovery`では、`service_name_list_for_tasks`でサービスの検出を行いましたが、`docker_label`を使ってサービスの検知を行うこともできます。
詳しくは以下のドキュメントを参照してください。 
https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/ContainerInsights-Prometheus-Setup-autodiscovery-ecs.html  
また、メトリクスの設定である`source_labels`, `dimensions`, `metric_selectors`あたりは一度動かしてみて、CloudWatchLogsに出力されるログを見て設定すると良いと思います。  
詳細については以下のドキュメントを参照してください。  
https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/ContainerInsights-Prometheus-Setup-configure-ECS.html  


### 3. タスク定義の作成
#### イメージ
作成したCWAイメージを指定します。

#### 環境変数
CWAコンテナの環境変数でPROMETHEUS_CONFIG_CONTENTとCWAGENT_CONFIGをSSMパラメータストアから持ってくるように設定します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3571776/468e759e-430c-72b0-478b-cb00c05cdcf5.png" width=800>


#### タスクロールの設定
以下のポリシーを付与したタスクロールを作成し、アタッチします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3571776/32ab40c8-752e-b9cd-6295-a19e4a2df05e.png" width=800>

ここでCWAからECSに対して、サービスやタスクのリストを取得するために、EcsCloudwatchagentDiscoveryServicePolicyというカスタムポリシーを作成しています。

```json
{
    "Version": "2012-10-17",
    "Statement": [ 
        {
            "Sid": "Statement1",
            "Effect": "Allow",
            "Action": [
                "ecs:ListTasks"
                "ecs:ListServices"
                "ecs:DescribeContainerInstances"
                "ecs:DescribeServices"
                "ecs:DescribeTasks"
                "ecs:DescribeTaskDefinition"
            ],
            "Resource": [
                "*"
            ]
        } 
    ]
}
```

### (必要であれば) VPCエンドポイントの作成
CWAコンテナをプライベートサブネットに配置する場合、ECSに対してサービスやタスクのリストを取得するために、`com.amazonaws.region.ecs`のVPCエンドポイントが必要になります。

## サービスの起動
クラスター内に、作成したタスク定義を基にtomcatサービスとCWAサービスを起動します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3571776/dc10fe5d-af3d-75bb-45c0-37205dd484e4.png" width=800>


# 結果の確認
CloudWatchLogsのロググループで、TomcatコンテナのJMXメトリクスを取ることができていることが確認できます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3571776/c76f7c20-a320-e050-1ef9-a0558b4f8f5f.png" width=800>


またCloudWatchMetricsで、CWagentというカスタムメトリクスが作成されており、指定したJMXの情報を可視化できることが確認できます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3571776/baed1006-9aa5-82af-c316-c24007f254dc.png" width=800>


# 終わりに
Fargate上で動かすTomcatコンテナ内のJMXの情報を jmx_exporter + CloudWatchAgent の構成で取得することが確認できました。コンテナであっても気軽にJMXの情報を得られるのは嬉しいですね。  
今回はCWAをサービスとして独立で動かしましたが、サイドカーコンテナとして動かすこともできるみたいです。
https://zenn.dev/wadahiro/articles/1c3c1fb5f030ec  
ただし、タスクIDなどを自動で付与できず（ECSにサービスの情報を取得しにいかないため）、ログからどのコンテナの情報なのかを判断するのが難しいのでは？と思うため、サービスとして動かす方が安牌なような気がします。  

※本ブログに記載した内容は個人の見解であり、所属する会社、組織とは関係ありません。