🚨

Amazon ECSイベントをCloudWatch Logsへ収集する

2023/11/02に公開

この記事は、3-shake Advent Calendar 2023 1日目のエントリ記事です。

きっかけ

ECSは、Container Insightsを有効化することでクラスタやサービスといった各レイヤのパフォーマンスメトリクスをCloudWatchに収集できる。

一方で、以下のようなケースにおいて一定の仮説を導くためには、このメトリクスだけではやや不足感があるため、発生したイベントやその結果を別の方式で監視したくなった。

  • メトリクスがスパイクしたタイミングで何が起きていたか?
  • デプロイを実行したが結果はどうだったか?
  • デプロイが失敗したが原因は何か?
    などなど・・

調べてみると、ECSはいくつかの種類のイベントをEventBridgeに送信しており、これをルールで拾ってCloudWatch Logsへ転送することができるらしいので、試してみた。

最終的に実現したこと

こんなことができるようになった(逆に、設定しないと見れないっていう・・)

  • CloudWatch Logsのロググループに、EventBridge経由でクラスタやサービス、コンテナインスタンス関連のECSイベントを転送するように設定できた(以下は、タスク起動不可によりサービスデプロイが失敗したイベント)

  • Container Insightsダッシュボード内でも、「ライフサイクルイベント」パネルでイベントを確認できるようになった

各種前提知識

具体的な検証に入る前に、各コンポーネントの前提知識について整理しておく。

EventBridge

  • ECSで発生したイベントは、EventBridgeのデフォルトイベントバスへ送信される(他のAWSサービスも同様)
  • このイベントはルールによって条件付きで抽出したり、任意のターゲットへ送信することができる(今回はCloudWatch Logsへ送信する)

ECS

イベント名 内容
Container Instance State Change Events タスクが起動して空きリソースが減った、といった理由でコンテナインスタンスの状態が変わった時に発生するイベント
Task State Change Events タスクが起動・停止によって状態変化した時に発生するイベント
Service Action Events サービスによるタスクスケジューリングなどに関するイベント。INFO・WARN・ERRORのレベルが存在する
Service Deoloyment State Change Events サービスのデプロイメントに関するイベント。INFOとERRORのレベルが存在する
  • (後述するが)Service Deoloyment State Change Eventsのみ、イベント内にECSクラスタ名の情報を持たないため、EventBridgeのルールを書く際に少し工夫が必要な場合もある

  • 今回検証するECSのイベントは、ロググループ/aws/events/ecs/containerinsights/<CLUSTER_NAME>/performanceに転送する(このロググループじゃなくてもOK)

  • このイベントは、上記ロググループに転送すると(冒頭で紹介した通り)Container Insightsダッシュボード上の「ライフサイクルイベント」というパネルで確認できる(但し、内容は薄い)

実装

Terraformで実装する例を以下紹介する。必要なリソースはCloudWatchのロググループとEventBridgeのルールの2つ。EventBridgeはaws_cloudwatch_event_ruleでも書けそうだったけど、CloudWatch Eventという名前はもうないので何となくでEventBridgeモジュールを使った。

CloudWatchロググループ

resource "aws_cloudwatch_log_group" "containerinsights_event" {
    name = "/aws/events/ecs/containerinsights/${module.ecs.cluster_name}/performance"
    retention_in_days = 3

   tags = {
        "ClusterName"   =   module.ecs.cluster_name
        "EventBridge-AssociatedRuleArn" = module.eventbridge.eventbridge_rule_arns["ECS_ContainerInsights"]
    }
}
  • 既に別のモジュールmodule.ecsで作成しているECSクラスタ名を利用
  • ロググループ名は何でもいいが、Container Insightsダッシュボードにも一応出したいのでこの形式
  • タグのEventBridge-AssociatedRuleArnは、送信元のEventBridgeルールの特定しやすさのために付与(動作には関係ない)

EventBridge

module "eventbridge" {
    source = "terraform-aws-modules/eventbridge/aws"

    create_bus = false

    rules = {
        ECS_ContainerInsights = {
            event_pattern = jsonencode({
                "source": ["aws.ecs"],
                "$or": [
                    {
                        "detail-type": ["ECS Deployment State Change"], 
                        "resources": [{
                            "prefix": replace(module.ecs.cluster_arn, ":cluster/", ":service/")
                        }]
                    },
                    {
                        "detail": { "clusterArn": [module.ecs.cluster_arn]}
                    } 
                ]})
            enabled = true
        }
    }

    targets = {
        ECS_ContainerInsights = [
            {
                name    = "performance"
                arn     = aws_cloudwatch_log_group.containerinsights_event.arn
            }
        ]
    }
}
  • ECSのイベントはデフォルトバスに送信されるが、bus_name = defaultと書くと「その名前は使えません」とエラーが出る。create_bus = falseとすることでバスを作成せずにdefaultを使用できる。
  • ECS_ContainerInsightsは配列のインデックスである。複数ルールを定義する場合は異なる文字列をインデックスとして定義を足していく。
  • event_patternは、「ECSのイベントである」AND「対象のクラスタ内のサービスの"ECS Deployment State Change"イベントである OR 対象クラスタのその他のイベントである」を条件とした。結構イベント数が多くなるので、コストも見つつ対象は精査したい。
  • event_patternはterraformでapplyすると以下の内容で設定された。理由は分からないが定義の順番が変わっている(一応意図した通りには動いた)。
{
  "$or": [{
    "detail-type": ["ECS Deployment State Change"],
    "resources": [{
      "prefix": "arn:aws:ecs:ap-northeast-1:XXXXXXXXXX:service/hogehoge_cluster"
    }]
  }, {
    "detail": {
      "clusterArn": ["arn:aws:ecs:ap-northeast-1:XXXXXXXXXX:cluster/hogehoge_cluster"]
    }
  }],
  "source": ["aws.ecs"]
}
  • 前述の通り、Service Deoloyment State Change Eventsはクラスタ名の情報を持たないのでclusterArn属性で引っ掛けることができない。かわりに、"arn:aws:ecs:<REGION>:<ACCOUNT_ID>:service/<CLUSTER_NAME>/<SERVICE_NAME>"という形式でサービス名を持っているので、Terraformでクラスタ名をreplace関数に投入し、サービス名に前方一致できる形に加工している。わかりにくいのでやらなくていいならやらない方がいい。
  • イベントパターンはいきなりTerraformで書こうとするときついので、あらかじめEventBridgeのサンドボックスで動作確認したものを使うとよい。

動作確認

イベントには、「特定の事象をトリガーに発信されるイベント」とは別に、死活監視のように一定周期ごとに発信されるイベントもあるようで、EventBridgeのモニタリングタブを見ると、特に何もしなくてもイベントが送信されているのが分かる。

FailedInvocationsメトリクスが他2つのメトリクスと似たような形で出ている場合、「イベントは発生してEventBridgeで受信できているが、それをターゲットに送信する際にエラーになっている」ということなので、設定に不備などが無いかを確認する。

コンテナインスタンスを停止させてみたり、サービスのタスクレプリカ数を変えてみたり、起動できないコンテナイメージをタスク定義に設定したりしてみると、(問題がなければ)CloudWatchのロググループ/aws/events/ecs/containerinsights/<CLUSTER_NAME>/performanceにイベントログが出力される。

試しに、タスクを3つ稼働させた状態でコンテナインスタンスを停止したら、こんなイベントが出た。
eventTypeERRORになっており、reasonAGENT:DISCONNECTEDになっているので、「コンテナインスタンスに何か起こっている」と判断できる。

{
    "version": "0",
    "id": "a1ebd563-c04b-b162-a872-bfba7c743c35",
    "detail-type": "ECS Service Action",
    "source": "aws.ecs",
    "account": "XXXXXXXXXXXXX",
    "time": "2023-11-02T07:56:11Z",
    "region": "ap-northeast-1",
    "resources": [
        "arn:aws:ecs:ap-northeast-1:XXXXXXXXXX:service/hogehoge_cluster/nginx"
    ],
    "detail": {
        "eventType": "ERROR",
        "eventName": "SERVICE_TASK_PLACEMENT_FAILURE",
        "clusterArn": "arn:aws:ecs:ap-northeast-1:XXXXXXXXX:cluster/hogehoge_cluster",
        "containerInstanceArns": [
            "arn:aws:ecs:ap-northeast-1:XXXXXXXXX:container-instance/hogehoge_cluster/123456789123456789"
        ],
        "capacityProviderArns": [
            "arn:aws:ecs:ap-northeast-1:XXXXXXXXX:capacity-provider/EC2"
        ],
        "reason": "AGENT:DISCONNECTED",
        "createdAt": "2023-11-02T07:56:11.370Z"
    }

もう1ケース、起動しないタスク定義を含むサービスをデプロイしてみると、以下のイベントが検知された。
reason"ECS deployment circuit breaker: tasks failed to start."と出ているので、タスクが何らかの理由で起動できず、サーキットブレーカーが起動したことがわかる。

{
    "version": "0",
    "id": "8efeb2e5-81f7-6050-4dd2-edb4bbbbfbbb",
    "detail-type": "ECS Deployment State Change",
    "source": "aws.ecs",
    "account": "XXXXXXXXXXX",
    "time": "2023-11-02T08:21:15Z",
    "region": "ap-northeast-1",
    "resources": [
        "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXX:service/hogehoge_cluster/malformed_task"
    ],
    "detail": {
        "eventType": "ERROR",
        "eventName": "SERVICE_DEPLOYMENT_FAILED",
        "deploymentId": "ecs-svc/0121204640679327819",
        "updatedAt": "2023-11-02T08:01:29.323Z",
        "reason": "ECS deployment circuit breaker: tasks failed to start."
    }
}

実運用での利用を想定した所感

  • タスクやコンテナインスタンスのイベントはリソースの情報なども細かく入っており、全体的にサイズが大きいためコストひっ迫要因になる。監視にクリティカルな情報がないイベントであればCloudWatchに送らなくても良さそう。
  • eventType属性を含むイベントのWARNERRORは監視に有用と思われるので活用したい。
  • クラスタの数が多くなり、ルールの管理が煩雑になる場合Terraformの可読性は考慮したほうが良い。複雑なルールを書かないで済むように、ルールや送信先ロググループの分割単位を考えたい。

参考資料

Discussion