Amazon ECS タスクの停止理由 (エラー内容) を CloudWatch Logs に保存する方法とその分析をしてみた

AWS FargateなどのECSタスクの停止理由を確認する場合、タスクが停止して1時間を過ぎると、コンソール上では確認できません。過去にさかのぼって確認できるよう、CloudWatch Logsに保存する方法について解説します。
2023.04.19

はじめに

AWS Fargateのタスクの停止理由を確認する方法が分からず詰まったため、今回記事を書きました。

ECSのコンソール上では、タスクが停止してから1時間以内であれば、以下のように停止理由であるエラー内容を確認できます。

ただし、上記のエラーは、1時間経過すると、以下のように見られなくなります。

過去にさかのぼって確認したい場合、CloudWatch logsなどに保存する必要があります。

今回は、ECSタスクの停止理由をさかのぼって確認するために、タスクが停止になった時のイベント情報をCloudWatch logsに保存する方法について、解説します。

構成図

構成図は、下記です。

汎用性がありますので、CloudFormationで、イベントルールとロググループを作成します。

流れとしては、ECSタスクが停止した時に、EventBridgeがトリガーとなり、ECSタスクの停止ログをCloudWatch Logsに保存します。

また、CloudWatch Logs Insightsでログデータを検索して分析をしてみます。

CloudFormationでスタックを作成

こちらのテンプレート通りに、スタックを作成します。

CloudFormationテンプレート (クリックすると展開します)

ecs-stopped-tasks-cwlogs.yaml

AWSTemplateFormatVersion: 2010-09-09
Description: Deploys the resources needed to store events of Amazon ECS stopped tasks in CloudWatch Logs

Parameters:
  CWLogGroupName:
    Type: String
    Description: The CloudWatch log group name to store the events
    Default: /aws/events/ECSStoppedTasksEvent
  CWLogGroupRetention:
    Type: Number
    Description: The number of days to retain the events in the log group
    AllowedValues: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653]
    Default: 30

Resources:
  EventRule:
    Type: AWS::Events::Rule
    Properties:
      Name: ECSStoppedTasksEvent
      Description: Triggered when an Amazon ECS Task is stopped
      EventPattern:
        source:
          - aws.ecs
        detail-type:
          - ECS Task State Change
        detail:
          desiredStatus:
            - STOPPED
          lastStatus:
            - STOPPED
      State: ENABLED
      Targets:
        - Arn: !GetAtt LogGroup.Arn
          Id: ECSStoppedTasks

  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Ref CWLogGroupName
      RetentionInDays: !Ref CWLogGroupRetention

  LogEventsPolicy:
    Type: AWS::Logs::ResourcePolicy
    Properties:
      PolicyName: LogEventsPolicy
      PolicyDocument: !Sub |
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Sid": "LogEventsPolicy",
              "Effect": "Allow",
              "Principal": {
                "Service": [
                  "delivery.logs.amazonaws.com",
                  "events.amazonaws.com"
                ]
              },
              "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
              ],
              "Resource": "${LogGroup.Arn}"
            }
          ]
        }

スタックを作成し、CREATE_COMPLETEになったことを確認します。

リソースの確認

スタックの作成により、リソースは2つ作成されますので、確認しておきます。

CloudWatch ロググループ

/aws/events/ECSStoppedTasksEventというロググループ名で作成されています。

EventBridgeのルール

ルール名ECSStoppedTasksEventで作成されています。

イベントパターンは、ECSがSTOPPEDのステータスであり、ターゲットは、CloudWatch ロググループの/aws/events/ECSStoppedTasksEventです。

イベントパターンを特定のクラスターにする

現状のEventBridgeのイベントパターンですと、すべてのクラスターの停止したタスク理由が保存されます。

{
  "source": ["aws.ecs"],
  "detail-type": ["ECS Task State Change"],
  "detail": {
    "desiredStatus": ["STOPPED"],
    "lastStatus": ["STOPPED"]
  }
}

クラスターを以下の通りに指定してあげることで、特定のクラスターのタスクのみをログに残すこともできます。

{
  "source": ["aws.ecs"],
  "detail-type": ["ECS Task State Change"],
  "detail": {
    "clusterArn": ["arn:aws:ecs:ap-northeast-1:[アカウントID]:cluster/[クラスター名]"],
    "desiredStatus": ["STOPPED"],
    "lastStatus": ["STOPPED"]
  }
}

ちなみに、特定のクラスターの特定のタスク定義に絞ることも可能です。

{
  "source": ["aws.ecs"],
  "detail-type": ["ECS Task State Change"],
  "detail": {
    "taskDefinitionArn": ["arn:aws:ecs:ap-northeast-1:[アカウントID]:task-definition/[タスク定義名]:[バージョン数]"],
    "clusterArn": ["arn:aws:ecs:ap-northeast-1:[アカウントID]:cluster/[クラスター名]"],
    "desiredStatus": ["STOPPED"],
    "lastStatus": ["STOPPED"]
  }
}

リソースの絞り方は、後述するCloudWatch Logsに出力される「ログの1例」を参考にしましょう。

ただし、今回は、すべてのクラスターでタスクの停止をログに残すようにします。

異常終了のタスク停止のみをログに出力する

ローリングアップデートによるデプロイのエラーメッセージ「Scaling activity initiated by〜」は検知しないように設定も可能です。

また、ContainersのexitCodeが0以外が異常終了であり、イベントパターンで除外することも可能です。

{
  "source": ["aws.ecs"],
  "detail-type": ["ECS Task State Change"],
  "detail": {
    "clusterArn": ["arn:aws:ecs:ap-northeast-1:[アカウントID]:cluster/[クラスター名]"],
    "desiredStatus": ["STOPPED"],
    "lastStatus": ["STOPPED"],
    "containers": {
      "exitCode": [
        {
          "anything-but": 0
        }
      ]
    },
    "stoppedReason": [
      {
        "anything-but": {
          "prefix": "Scaling activity initiated by"
        }
      }
    ]
  }
}

タスク停止してみる

ECSのタスクの起動を失敗させるため、プライベートサブネット内でパブリック IPを付与させる設定でタスクを起動し、タスクを停止させました。

/aws/events/ECSStoppedTasksEventを見てみると、json形式でログが出力されていました。

~省略~
"stoppedReason": "ResourceInitializationError: unable to pull secrets or registry auth: execution resource retrieval failed: unable to retrieve ecr registry auth: service call has been retried 3 time(s): RequestError: send request failed caused by: Post \"https://api.ecr.ap-northeast-1.amazonaws.com/\": dial tcp 3.112.64.212:443: i/o timeout. Please check your task network configuration."
~省略~
「ログの1例」です。 (クリックすると展開します)
{
    "version": "0",
    "id": "c4b21fc6-c933-7f28-00cd-4f7162c5b304",
    "detail-type": "ECS Task State Change",
    "source": "aws.ecs",
    "account": "[アカウントID]",
    "time": "2023-04-14T18:07:00Z",
    "region": "ap-northeast-1",
    "resources": [
        "arn:aws:ecs:ap-northeast-1:[アカウントID]:task/nginx/dcf8140fcd23495bb7c8bb3919fd485a"
    ],
    "detail": {
        "attachments": [
            {
                "id": "67f94f8d-b24a-4066-bcb6-e47df4bf2e93",
                "type": "elb",
                "status": "DELETED",
                "details": []
            },
            {
                "id": "e2b1dc3a-ade7-4188-92ff-d36fe3ebf504",
                "type": "eni",
                "status": "DELETED",
                "details": [
                    {
                        "name": "subnetId",
                        "value": "subnet-0bbe65b1222f92210"
                    },
                    {
                        "name": "networkInterfaceId",
                        "value": "eni-08caee15559291301"
                    },
                    {
                        "name": "macAddress",
                        "value": "0a:bc:9f:87:01:5f"
                    },
                    {
                        "name": "privateDnsName",
                        "value": "ip-10-0-1-72.ap-northeast-1.compute.internal"
                    },
                    {
                        "name": "privateIPv4Address",
                        "value": "10.0.1.72"
                    }
                ]
            }
        ],
        "attributes": [
            {
                "name": "ecs.cpu-architecture",
                "value": "x86_64"
            }
        ],
        "availabilityZone": "ap-northeast-1c",
        "capacityProviderName": "FARGATE_SPOT",
        "clusterArn": "arn:aws:ecs:ap-northeast-1:[アカウントID]:cluster/nginx",
        "connectivity": "CONNECTED",
        "connectivityAt": "2023-04-14T17:29:26.067Z",
        "containers": [
            {
                "containerArn": "arn:aws:ecs:ap-northeast-1:[アカウントID]:container/nginx/dcf8140fcd23495bb7c8bb3919fd485a/3ee885e1-3001-4e2f-bb31-d843bdfa2cff",
                "lastStatus": "STOPPED",
                "name": "nginx",
                "image": "[アカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/nginx:dev",
                "imageDigest": "sha256:fd0f22e46bd78637783815f3ea5f98ff3d09de764789eaccf1ca412d97743563",
                "runtimeId": "dcf8140fcd23495bb7c8bb3919fd485a-2531612879",
                "taskArn": "arn:aws:ecs:ap-northeast-1:[アカウントID]:task/nginx/dcf8140fcd23495bb7c8bb3919fd485a",
                "networkInterfaces": [
                    {
                        "attachmentId": "e2b1dc3a-ade7-4188-92ff-d36fe3ebf504",
                        "privateIpv4Address": "10.0.1.72"
                    }
                ],
                "cpu": "0"
            }
        ],
        "cpu": "256",
        "createdAt": "2023-04-14T17:29:22.086Z",
        "desiredStatus": "STOPPED",
        "enableExecuteCommand": false,
        "ephemeralStorage": {
            "sizeInGiB": 20
        },
        "group": "service:nginx",
        "launchType": "FARGATE",
        "lastStatus": "STOPPED",
        "memory": "512",
        "overrides": {
            "containerOverrides": [
                {
                    "name": "nginx"
                }
            ]
        },
        "platformVersion": "1.4.0",
        "pullStartedAt": "2023-04-14T17:29:53.482Z",
        "pullStoppedAt": "2023-04-14T17:29:58.077Z",
        "startedAt": "2023-04-14T17:30:13.12Z",
        "startedBy": "ecs-svc/4374651048025955904",
        "stoppingAt": "2023-04-14T18:00:53.753Z",
        "stoppedAt": "2023-04-14T18:07:00.681Z",
        "stoppedReason": "stoppedReason": "ResourceInitializationError: unable to pull secrets or registry auth: execution resource retrieval failed: unable to retrieve ecr registry auth: service call has been retried 3 time(s): RequestError: send request failed caused by: Post \"https://api.ecr.ap-northeast-1.amazonaws.com/\": dial tcp 3.112.64.212:443: i/o timeout. Please check your task network configuration.",
        "stopCode": "SpotInterruption",
        "taskArn": "arn:aws:ecs:ap-northeast-1:[アカウントID]:task/nginx/dcf8140fcd23495bb7c8bb3919fd485a",
        "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:[アカウントID]:task-definition/nginx:1",
        "updatedAt": "2023-04-14T18:07:00.681Z",
        "version": 8
    }
}

これで、いつでもタスクの停止理由を確認することができます。

CloudWatch Logs Insightsを利用し、分析する

CloudWatch Logs Insights を使用すると、ログデータを簡単に検索して分析できます。

今回は、停止したタスクのうち、確認したいタスクの停止理由を検索するクエリをご紹介します。

クエリは、2つ実行します。

  1. 停止したタスクのタスクIDを検索する
    • クエリ:fields detail.taskArn | filter detail.clusterArn like "クラスター名" and detail.group like "サービス名"
  2. 停止したタスクのうち、確認したいタスクのタスクIDから、タスク理由を表示する
    • クエリ:fields detail.stoppedReason, detail.containers.0.name, detail.containers.0.exitCode | filter detail.taskArn like "タスクID"

詳しいCloudWatch Logs Insights のクエリ構文は、こちらのドキュメントを一読ください

実際の操作画面を表示しながら、クエリを実行します。

私の場合、クラスター名は、nginx、サービス名は、nginx-serviceです。

  1. [Logs Insightsで表示]をクリックします。
  2. ロググループ時間を設定後、1つめのクエリを実行しますと、タスクのIDが表示されます。今回、タスクIDのc92d1197454846d092efca65a4429599のタスク停止理由を見てみます。
  3. 2つめのクエリを入力し実行します。
    • 停止理由は、Task stopped by userつまりユーザーがタスクを停止したと分かりました
    • exitCodeは、こちらが参考になります。今回は、0とあるため、ユーザーによって停止と分かります。

他のタスクIDを実行すると、detail.containers.0.exitCodeがないものもありますね。

他の停止理由やエラー内容は、公式ドキュメントで解説されているため、ご参考ください。

参考