ECS Task毎のEphemeral Storage使用量を可視化する

## 背景と概要
troccoではサービスのUIを実行する環境としてECSを採用しています。
ECSのTask Definitionでは各Taskに割り当てるEphemeral Storage容量を指定しますが、その容量以上のファイル書き込みが発生し該当のWebページが表示できなくなるトラブルが発生しました。それ以後、特定のタスクのEphemeral Storage使用率がしきい値を上回った場合はSlackへ通知し、トラブルを未然に防げるような対応を行いました。
しかしCloudWatch Metricsではタスク毎の使用率ではなく全タスクの平均使用率しか確認することができないため、Ephemeral Storageが逼迫しているタスクを特定できないという課題がありました。

弊社の環境のECS ClusterではContainer Insightsを有効化していたので、タスク毎のメトリクスはCloudWatch Logsに保存されていました。
本記事ではContainer InsightがCloudWatch Logsに出力しているログに対しQueryを実行することで、タスク毎のEphemeral Storage使用量を可視化する方法についてまとめました。

## 前提
- ECSのTaskはFargateで稼働している
- Fargate Linuxプラットフォームのバージョンが1.4.0以降
- ECS ClusterでContainer Insightsが有効化されている

## Container Insightsのログ
Container InsightsパフォーマンスログイベントはCloudWatch Logsの`/aws/ecs/containerinsights/[クラスタ名]/performance`という名前のロググループに保存されています。このロググループ内には以下のログストリームが作成されています。
- ClusterTelemetry-[クラスタ名]
- ServiceTelemetry-[サービス名]
- FargateTelemetry-xxxx

`FargateTelemetry-xxxx`内にはTaskとContainerに関するメトリクスのログが1分毎に記録されています。今回はタスク毎のメトリクスを知りたいため、`FargateTelemetry-xxxx`の内容を調べてみます。

ログの内容については以下の通りです。
<details><summary>FargateTelemetry-xxxx</summary><div>

```
{
    "Version": "0",
    "Type": "Task",
    "TaskId": "ad1fb6180dfb45a5a8ed3ad7ca7f3ac3",
    "TaskDefinitionFamily": [TaskDefinitionFamily名],
    "TaskDefinitionRevision": "234",
    "ServiceName": [サービス名],
    "ClusterName": [クラスタ名],
    "AccountID": [AWSアカウントID],
    "Region": "ap-northeast-1",
    "AvailabilityZone": "ap-northeast-1a",
    "KnownStatus": "RUNNING",
    "LaunchType": "FARGATE",
    "PullStartedAt": 1703136502731,
    "PullStoppedAt": 1703136532565,
    "CreatedAt": 1703136485850,
    "StartedAt": 1703136569195,
    "Timestamp": 1704442680000,
    "CpuUtilized": 458.5331119791667,
    "CpuReserved": 4096,
    "MemoryUtilized": 7152,
    "MemoryReserved": 12288,
    "StorageReadBytes": 0,
    "StorageWriteBytes": 29413376,
    "NetworkRxBytes": 55973,
    "NetworkRxDropped": 0,
    "NetworkRxErrors": 0,
    "NetworkRxPackets": 28421445,
    "NetworkTxBytes": 18951,
    "NetworkTxDropped": 0,
    "NetworkTxErrors": 0,
    "NetworkTxPackets": 23265409,
    "EphemeralStorageUtilized": 3.18,
    "EphemeralStorageReserved": 42.94,
    "CloudWatchMetrics": [
        {
            "Namespace": "ECS/ContainerInsights",
            "Metrics": [
                {
                    "Name": "CpuUtilized",
                    "Unit": "None"
                },
                {
                    "Name": "CpuReserved",
                    "Unit": "None"
                },
                {
                    "Name": "MemoryUtilized",
                    "Unit": "Megabytes"
                },
                {
                    "Name": "MemoryReserved",
                    "Unit": "Megabytes"
                },
                {
                    "Name": "StorageReadBytes",
                    "Unit": "Bytes/Second"
                },
                {
                    "Name": "StorageWriteBytes",
                    "Unit": "Bytes/Second"
                },
                {
                    "Name": "NetworkRxBytes",
                    "Unit": "Bytes/Second"
                },
                {
                    "Name": "NetworkTxBytes",
                    "Unit": "Bytes/Second"
                },
                {
                    "Name": "EphemeralStorageUtilized",
                    "Unit": "Gigabytes"
                },
                {
                    "Name": "EphemeralStorageReserved",
                    "Unit": "Gigabytes"
                }
            ],
            "Dimensions": [
                [
                    "ClusterName"
                ],
                [
                    "ServiceName",
                    "ClusterName"
                ],
                [
                    "ClusterName",
                    "TaskDefinitionFamily"
                ]
            ]
        }
    ]
}
```
</div></details>


このログの中でEphemeral Storageに関するメトリクスは以下の2つになります。
- EphemeralStorageUtilized(Ephemeral Storageの使用バイト数)
- EphemeralStorageReserved(Ephemeral Storageの予約されたバイト数)


## CloudWatch Logs Insightsのクエリ
CloudWatch Logs InsightsではCloudWatch Logsに送信されたログデータを検索することができます。上記のJSONから取得したいデータは**最新のリビジョンにおける、task ID毎のEphemeral Storage使用バイト数の最大値** になります。Dashboardに表示する際の視認性も考慮してEphemeral Storage使用率もあわせて計算しておきます。

値を取得するため、Cloudwatch Logs Insightsのコマンドを紹介します。
- `fields`: クエリ結果に特定のフィールドを表示し、新しいフィールドを作成したりするときに使用できる関数と演算をサポート
- `filter`: クエリをフィルタリング
- `stats`: 統計を算出
- `sort`: 並び替え

また、複数のコマンドを含むクエリではコマンドをパイプ文字 (|) で区切ります。

上記を踏まえて今回作成したクエリは以下の通りです。

```
fields @message |
filter Type="Task" | 
filter TaskDefinitionFamily=<対象のTask Deifinition Family名> |
filter @logStream like /FargateTelemetry/ | 
stats latest(TaskDefinitionRevision) as Rev,
  max(EphemeralStorageReserved) as `Reserved(GB)`, 
  max(EphemeralStorageUtilized) as `PeakUtilized(GB)`, 
  max(EphemeralStorageUtilized) * 100 / max(EphemeralStorageReserved) as `PeakUtilized(%)` 
  by TaskId |
sort Rev desc
```

クエリを基に各コマンドの解説をします。
- `fields`
    - `@message`はInputLogEventのmessageフィールドに相当します(CloudWatch LogsのコンソールでLog streamを閲覧した際に表示されるmessageというヘッダーに該当)
    - 上記のJSONがそのまま入っています
- `filter`
    - Type
        - TaskとContainerとに関するログが含まれています。今回はTaskに間するログを見たいのでTaskを指定します
    - TaskDefinitionFamily
        - Cluster内で複数のTask Definitionが存在する場合は必要なものにしぼります
    - `@logStream`
        - 上述の通り3種類のログストリームが存在するため、`FargateTelemetry-xxxx`にしぼります。`@logStream`は自動的に生成されるフィールドで、ログストリームが格納されています
- `stats`
    - `EphemeralStorageReserved`と`EphemeralStorageUtilized`に加え、この2つの値を使用して最大使用率を算出します
    - `as`を使用して結果にエイリアスを指定します
    - 上記の値をTask ID毎に算出したいので、末尾に`by TaskId`を記載します
- `sort`
    - 複数のRevisionで取得される可能性があるのでRevisionの降順でソートする

## CloudWatch Dashboardにクエリを追加
上記で生成したクエリをCloudWatch Dashboardに追加します。弊社ではAWSのresourceをTerraformで管理しているためDashboardもTerraformで管理します。

CloudWatch Dashboardのwidgetはjsonで定義されます。定義内容は以下の通りです。

```json
{
    "widgets": [
     {
        "type": "log",
        "x": 0,
        "y": 0,
        "width": 24,
        "height": 9,
        "properties": {
          "query": "SOURCE '/aws/ecs/containerinsights/${cluster_name}/performance' | fields @message\n| filter Type=\"Task\"\n| filter TaskDefinitionFamily=\"${task_definition_family}\"\n| filter @logStream like /FargateTelemetry/\n| stats latest(TaskDefinitionRevision) as Rev, max(EphemeralStorageReserved) as `Reserved(GB)`, max(EphemeralStorageUtilized) as `PeakUtilized(GB)`, max(EphemeralStorageUtilized) * 100 / max(EphemeralStorageReserved) as `PeakUtilized(%)` by TaskId\n| sort Rev desc\n",
          "stacked": false,
          "title": "${task_definition_family} Storage Details",
          "view": "table"
        }
     }
    ]
  }
```

ポイントをいくつか解説します。
- `properties`内の`query`フィールドに実行するqueryを定義します。先頭に`SOURCE`とlog groupの名前を付与します
- `view`で表示形式を指定できます。今回は表形式で見たいため`table`を指定します
- Terraformの`templatefile`関数を使用することで、1つのjsonファイルを複数のTaskや環境で使い回すことができます。今回の例では、ECSのクラスタ名(`cluster_name`)とタスク定義名(`task_definition_family`)を変数として扱っています
 
以下がTerraformのサンプルコードになります。

```terraform
resource "aws_cloudwatch_dashboard" "ecs_storage_details" {
  dashboard_name = "ecs-storage-details"
  dashboard_body = templatefile("${path.module}/../template/ecs-storage-details-dashboard.json", {
    cluster_name           = data.aws_ecs_cluster.sample_cluster.cluster_name
    task_definition_family = data.aws_ecs_task_definition.sample_task.family
  })
}
```

## 実際のDashboard
上記の設定で作成したDashboardの一例です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269592/b4e95237-dac5-e671-09d1-59a93ac0bf73.png)


## まとめ
Container Insightsが出力するログに対し、CloudWatch Logs Insightsのクエリを使用することでCloudWatch Metricsの標準機能では確認することができない詳細なメトリクスを取得することができます。また、CloudWatch Dashboardにクエリを紐付けることで、クエリで取得した結果を可視化することもできました。


## 参考
- [Amazon ECS の Container Insights パフォーマンスログイベント - Amazon CloudWatch](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/Container-Insights-reference-performance-logs-ECS.html)
- [CloudWatch Logs Insights クエリ構文 - Amazon CloudWatch Logs](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/CWL_QuerySyntax.html)
- [サポートされるログと検出されるフィールド - Amazon CloudWatch Logs](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/CWL_AnalyzeLogData-discoverable-fields.html)
- [クエリをダッシュボードに追加する、またはクエリ結果をエクスポートする - Amazon CloudWatch Logs](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/CWL_ExportQueryResults.html)
- [aws_cloudwatch_dashboard | Resources | hashicorp/aws | Terraform | Terraform Registry](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_dashboard)
- [Dashboard Body Structure and Syntax - Amazon CloudWatch](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/APIReference/CloudWatch-Dashboard-Body-Structure.html#CloudWatch-Dashboard-Properties-Log-Widget-Object)