Application SignalsでECSで動かしているMetabaseのサービスマップを作成する

# TL;DR
Application Signals で Metabase のサービスマップを作成してみました。

![スクリーンショット 2024-08-26 21.59.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/08281008-b5cb-133b-2b76-899a480aec3e.png)


# Amazon CloudWatch Application Signalsとは
CloudWatch Application Signalsを使用するとアプリケーションの状態やメトリクスやトレースが自動で収集され、呼び出し量、可用性、エラーなどの指標をサービスマップなどの形で可視化することができます。
今まで似たサービスに AWS X-Ray がありましたが、SDK をアプリケーションコードに実装する必要があり、ハードルが高かったです。
Amazon CloudWatch Application Signalsを使用することでアプリケーションコードに手を加えることなく、導入することが可能です。

[Application Signals](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch-Application-Monitoring-Sections.html)


# 今回の目的
今回は ECS Fargate で動かしている Metabase に Application Signales を導入し、サービスマップを作成してみようと思います。
参考にした公式ドキュメントはこちらです。

[Use a custom setup to enable Application Signals on Amazon ECS](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Application-Signals-Enable-ECS.html)


## 1. アカウントでアプリケーションシグナルを有効にする
AWSアカウントで Application Signals を有効にしていない場合は、サービスを検出するために必要なアクセス許可を Application Signals に付与する必要があります。
アカウントに対して 1 回だけ行う必要があります。

1. CloudWatchコンソールを開きます。 [https://console.aws.amazon.com/cloudwatch/](https://console.aws.amazon.com/cloudwatch/)
2. ナビゲーションペインで Application Signals の Service を選択します
3. 「サービスの検出を開始」を選択します
4. チェックを入れ、「サービスの検出を開始」を選択します。IAMロールに `AWSServiceRoleForCloudWatchApplicationSignals` が作成されます

![スクリーンショット 2024-08-26 15.14.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/e1135425-9aaf-1efb-68c8-e1b51d4ea2e0.png)

![スクリーンショット 2024-08-26 15.14.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/fe844ef0-889f-dc48-6795-809d0f971310.png)



## 2. IAMロールの作成
ECS用のIAMロールを作成します。
通常の権限に加えて、 `CloudWatchAgentServerPolicy` を追加する必要があります。
今回は `kabigon-ecs-task-role` という IAM ロールを作成します。
`AmazonECSTaskExecutionRolePolicy`, `AmazonSSMReadOnlyAccess`, `CloudWatchAgentServerPolicy` をアタッチしました。



![screencapture-us-east-1-console-aws-amazon-iam-home-2024-08-26-21_06_54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/7ebc0d35-6836-663c-3ec3-4569cd3855dd.png)


![スクリーンショット 2024-08-26 21.07.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/a41bb4f8-515d-b18b-9adf-e01238ebde2d.png)


```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "Service": "ecs-tasks.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```


## 3. CloudWatch エージェント構成の準備
SSM パラメーターストアに CloudWatchエージェントの設定をアップロードします。
`/tmp/ecs-cwagent.json` という名前でローカル環境に以下の内容のファイルを作成します。

```json
{
  "traces": {
    "traces_collected": {
      "application_signals": {}
    }
  },
  "logs": {
    "metrics_collected": {
      "application_signals": {}
    }
  }
}
```

以下のコマンドを実行し、パラメーターストアに `ecs-cwagent` という名前でアップロードします。

```bash
aws ssm put-parameter \
--name "ecs-cwagent" \
--type "String" \
--value "`cat /tmp/ecs-cwagent.json`" \
--region "ap-northeast-1"
```

![screencapture-ap-northeast-1-console-aws-amazon-systems-manager-parameters-ecs-cwagent-description-2024-08-26-21_10_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/b6f95134-4315-bf55-a019-ad06e5ecf969.png)


## 4. ECSクラスターの作成
Metabaseらのコンテナを動かすECSクラスターを作成します。
`kabigon-metabase-cluster` という名前でクラスターを作成しました。


## 5. タスク定義の作成
`kabigon-metabase-task-definition` という名前でタスク定義を作成します。
タスクロール、タスク実行ロールは先ほど作成した `kabigon-exs-task-role` をアタッチします。
細かい設定は画像およびJSONを参考にしてください。

### 5.1 Metabase用のコンテナ
Metabase用のコンテナを作成します。

**コンテナ名**
`metabase`

**イメージ**
`metabase/metabase`

**環境変数**

| name | value |
| ---- | ---- |
| JAVA_TOOL_OPTIONS | &nbsp;-javaagent:/otel-auto-instrumentation/javaagent.jar |
| OTEL_AWS_APPLICATION_SIGNALS_ENABLED | true |
| OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT | http://localhost:4316/v1/metrics |
| OTEL_EXPORTER_OTLP_PROTOCOL | http/protobuf |
| OTEL_EXPORTER_OTLP_TRACES_ENDPOINT | none |
| OTEL_LOGS_EXPORTER | none |
| OTEL_METRICS_EXPORTER | none |
| OTEL_PROPAGATORS | tracecontext,baggage,b3,xray |
| OTEL_RESOURCE_ATTRIBUTES | service.name=metabase |
| OTEL_TRACES_SAMPLER | xray |

:::note warn
タスク定義はJSONから編集してください
:::

`JAVA_TOOL_OPTIONS` の値の先頭にはスペースを入れる必要があります。
JSONではなくGUIでタスク定義を編集すると先頭のスペースが削除されるので、JSONから編集するのがおすすめです。


### 5.2 ECS CloudWatch Agentのコンテナ
ECS CloudWatch Agentのコンテナを作成します。

**コンテナ名**
`ecs-cwagent`

**イメージ**
`public.ecr.aws/cloudwatch-agent/cloudwatch-agent:latest`

**環境変数**
| name | value(ParameterStoreより) |
| ---- | ---- |
| CW_CONFIG_CONTENT | ecs-cwagent |


### 5.3 init用のコンテナ
init用のコンテナを作成します。
`javaagent.jar` を共通のボリュームにコピーしています。

**コンテナ名**
`init`

**イメージ**
`public.ecr.aws/aws-observability/adot-autoinstrumentation-java:v1.32.3`

**コマンド**
`cp /javaagent.jar /otel-auto-instrumentation/javaagent.jar`


### 5.4 ボリュームの設定
`opentelemetry-auto-instrumentation` というボリュームを作成し、metabaseコンテナとinitコンテナでマウントします。

![スクリーンショット 2024-08-26 21.47.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/b335f246-2bbf-d04f-b5ae-ace8d1eddf81.png)

### 5.5 全体の設定
全体の設定はこちらです。

<details><summary>画像</summary>

![screencapture-ap-northeast-1-console-aws-amazon-ecs-v2-task-definitions-kabigon-metabase-task-definition-13-create-revision-2024-08-26-21_49_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/92a8331b-d616-51cb-b6f3-b1a4e0477d66.png)

</details>



<details><summary>JSON</summary>

```
{
    "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:{AWS_ACCOUNT_ID}:task-definition/kabigon-metabase-task-definition:13",
    "containerDefinitions": [
        {
            "name": "metabase",
            "image": "metabase/metabase",
            "cpu": 0,
            "portMappings": [
                {
                    "name": "3000",
                    "containerPort": 3000,
                    "hostPort": 3000,
                    "protocol": "tcp",
                    "appProtocol": "http"
                }
            ],
            "essential": true,
            "environment": [
                {
                    "name": "OTEL_EXPORTER_OTLP_PROTOCOL",
                    "value": "http/protobuf"
                },
                {
                    "name": "OTEL_AWS_APPLICATION_SIGNALS_ENABLED",
                    "value": "true"
                },
                {
                    "name": "OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT",
                    "value": "http://localhost:4316/v1/metrics"
                },
                {
                    "name": "OTEL_RESOURCE_ATTRIBUTES",
                    "value": "service.name=metabase"
                },
                {
                    "name": "OTEL_METRICS_EXPORTER",
                    "value": "none"
                },
                {
                    "name": "JAVA_TOOL_OPTIONS",
                    "value": " -javaagent:/otel-auto-instrumentation/javaagent.jar"
                },
                {
                    "name": "OTEL_LOGS_EXPORTER",
                    "value": "none"
                },
                {
                    "name": "OTEL_TRACES_SAMPLER",
                    "value": "xray"
                },
                {
                    "name": "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
                    "value": "http://localhost:4316/v1/traces"
                },
                {
                    "name": "OTEL_PROPAGATORS",
                    "value": "tracecontext,baggage,b3,xray"
                }
            ],
            "mountPoints": [
                {
                    "sourceVolume": "opentelemetry-auto-instrumentation",
                    "containerPath": "/otel-auto-instrumentation",
                    "readOnly": false
                }
            ],
            "volumesFrom": [],
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/kabigon-metabase-task-definition",
                    "mode": "non-blocking",
                    "awslogs-create-group": "true",
                    "max-buffer-size": "25m",
                    "awslogs-region": "ap-northeast-1",
                    "awslogs-stream-prefix": "ecs"
                }
            },
            "systemControls": []
        },
        {
            "name": "ecs-cwagent",
            "image": "public.ecr.aws/cloudwatch-agent/cloudwatch-agent:latest",
            "cpu": 0,
            "portMappings": [],
            "essential": true,
            "environment": [],
            "mountPoints": [],
            "volumesFrom": [],
            "secrets": [
                {
                    "name": "CW_CONFIG_CONTENT",
                    "valueFrom": "ecs-cwagent"
                }
            ],
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/kabigon-metabase-task-definition",
                    "mode": "non-blocking",
                    "awslogs-create-group": "true",
                    "max-buffer-size": "25m",
                    "awslogs-region": "ap-northeast-1",
                    "awslogs-stream-prefix": "ecs"
                }
            },
            "systemControls": []
        },
        {
            "name": "init",
            "image": "public.ecr.aws/aws-observability/adot-autoinstrumentation-java:v1.32.3",
            "cpu": 0,
            "portMappings": [],
            "essential": false,
            "command": [
                "cp",
                "/javaagent.jar",
                "/otel-auto-instrumentation/javaagent.jar"
            ],
            "environment": [],
            "mountPoints": [
                {
                    "sourceVolume": "opentelemetry-auto-instrumentation",
                    "containerPath": "/otel-auto-instrumentation",
                    "readOnly": false
                }
            ],
            "volumesFrom": [],
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/kabigon-metabase-task-definition",
                    "mode": "non-blocking",
                    "awslogs-create-group": "true",
                    "max-buffer-size": "25m",
                    "awslogs-region": "ap-northeast-1",
                    "awslogs-stream-prefix": "ecs"
                }
            },
            "systemControls": []
        }
    ],
    "family": "kabigon-metabase-task-definition",
    "taskRoleArn": "arn:aws:iam::{AWS_ACCOUNT_ID}:role/kabigon-ecs-task-role",
    "executionRoleArn": "arn:aws:iam::{AWS_ACCOUNT_ID}:role/kabigon-ecs-task-role",
    "networkMode": "awsvpc",
    "revision": 13,
    "volumes": [
        {
            "name": "opentelemetry-auto-instrumentation",
            "host": {}
        }
    ],
    "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.28"
        },
        {
            "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": "1024",
    "memory": "3072",
    "runtimePlatform": {
        "cpuArchitecture": "X86_64",
        "operatingSystemFamily": "LINUX"
    },
    "registeredAt": "2024-08-26T11:00:14.580Z",
    "registeredBy": "arn:aws:iam::{AWS_ACCOUNT_ID}:user/kabigon.ono",
    "tags": []
}
```

</details>


## 6. タスクの起動
`kabigon-metabase-task-definition` からタスクを作成し、`metabase`, `ecs-cwagent`, `init` の各コンテナが起動することを確認します。
`init` コンテナは起動後すぐに終了します。


## 7. サービスマップの確認
しばらくするとサービスマップを確認することができます。

![スクリーンショット 2024-08-26 21.59.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/08281008-b5cb-133b-2b76-899a480aec3e.png)


# まとめ
Application Signalsで簡単にサービスマップを作成することができました。
今後はボトルネックの特定などがかなり容易に行うことができそうです。
まだまだ新しいサービスで知らないことばかりなので、積極的に使っていこうと思います。