LoginSignup
28
17

More than 3 years have passed since last update.

ECSタスクの終了ステータスを見てLambdaを発火させる

Last updated at Posted at 2019-12-05

LIFULLでSETとしてテスト自動化やCI/CDの改善をやってます。
弊チームの自動テストはECSタスクで実行しています。
その自動テストの運用改善の一環で調べたことを書き綴ります。

自動システムテストのアーキテクチャ

下記の流れでテスト実行・テスト終了通知を行っていました。

Scheduled task(定時実行)

ECS Run task(自動テスト実行)

Cloudwatch Rules(ECS Taskの完了を検知)

Lambda(チャットツールへ通知)

要求・要件

従来のままだとテストがすべてPassしても通知が来てしまいました。
ある自動テストプロジェクトの際に「テストがFailしたときだけチャットに通知させたい」という要求が生まれました。
よく考えれば当たり前の要求です。(たまたま私達の運用ではいらなかった)

要件に落とし込むと
「ECS Run taskで実行したテスト実行コンテナの終了ステータスが異常値の場合だけLambdaを発火させたい」
となりました。

やったこと①:ECSイベントを調査

ECSタスクはある状態から別の状態に切り替わるときにイベントが発生します。

イベントには2つあります。
1. コンテナインスタンス状態変更イベント
2. タスク状態変更イベント

今回はタスクの状態を利用したいので、2番のイベントタイプでした。

イベントを確認する

aws cliを使って送信されるイベントを表示させて以下を確認しました。

  • Fail時のrun-test(テスト実行)コンテナの終了ステータスを確認
  • 後述のEventパターンで使えそうな条件がないか確認
# 実際のTask実行後のTask-IDを使います
$ aws ecs describe-tasks --cluster run_test_cluster --tasks arn:aws:ecs:ap-northeast-1:0000000000000:task/run_test_cluster/0000000-11aa-22bb-33cc-444444444444 

# 以下の階層のJSONが取得できます
# 一部情報を空にしています

{
  "tasks":
  [
    {
      "taskArn": "value"
      "clusterArn": ""
      "taskDefinitionArn": ""
      "overrides": {},
      "lastStatus": "STOPPED",
      "desiredStatus": "STOPPED",
      "cpu": "4096",
      "memory": "8192",
      "containers": [
        {
          "containerArn": "",
          "taskArn": "",
          "name": "run-test",
          "lastStatus": "STOPPED",
          "exitCode": 1,
          "networkBindings": [],
          "networkInterfaces": [
              {
                  "attachmentId": "",
                  "privateIpv4Address": ""
              }
          ],
          "healthStatus": "UNKNOWN",
          "cpu": "0"
        },
        {
            "containerArn": "",
            "taskArn": "",
            "name": "selenium-node-chrome",
            "lastStatus": "STOPPED",
            "exitCode": 143,
            "networkBindings": [],
            "networkInterfaces": [
                {
                    "attachmentId": "",
                    "privateIpv4Address": ""
                }
            ],
            "healthStatus": "UNKNOWN",
            "cpu": "200",
            "memory": "400",
            "memoryReservation": "400"
        },
        {
            "containerArn": "",
            "taskArn": "",
            "name": "selenium-hub",
            "lastStatus": "STOPPED",
            "exitCode": 143,
            "networkBindings": [],
            "networkInterfaces": [
                {
                    "attachmentId": "",
                    "privateIpv4Address": ""
                }
            ],
            "healthStatus": "UNKNOWN",
            "cpu": "0",
            "memory": "200",
            "memoryReservation": "200"
        }
      ],
            "version": 1,
            "stoppedReason": "Essential container in task exited",
            "stopCode": "EssentialContainerExited",
            "connectivity": "CONNECTED",
            "connectivityAt": 123456789.123,
            "pullStartedAt": 123456789.123,
            "pullStoppedAt": 123456789.123,
            "executionStoppedAt": 123456789.123,
            "createdAt": 123456789.123,
            "startedAt": 123456789.123,
            "stoppingAt": 123456789.123,
            "stoppedAt": 123456789.123,
            "group": "family:run-test-task",
            "launchType": "FARGATE",
            "platformVersion": "1.3.0",
            "healthStatus": "UNKNOWN",
            "tags": []
        }
    ],
    "failures": []
}

containersの中にあるexitCodenameが使えそうなことがわかりました。

やったこと②:Cloudwatch Rule Eventパターンの調査

Eventパターンを定義するとそのパターンに合致した場合にだけCloudwatch Ruleに紐付いた処理を行います。

Before: Taskが終了したら毎回動くEventパターン

今回の改善前から使っていたものです。
対象のECS Clusterのタスクが終了したら後続処理が実行されます。
テストのPass/Fail関係なく動きます。

{
  "source": [
    "aws.ecs"
  ],
  "detail-type": [
    "ECS Task State Change"
  ],
  "detail": {
    "clusterArn": [
      "arn:aws:ecs:ap-northeast-1:0000000000000:cluster/run_test_cluster"
    ],
    "lastStatus": [
      "STOPPED"
    ],
    "stoppedReason": [
      "Essential container in task exited"
    ]
  }
}

After: 特定のコンテナが終了し、終了ステータスが1のときに動く

先程のexitCodenameを条件に追加したものです。

{
  "source": [
    "aws.ecs"
  ],
  "detail-type": [
    "ECS Task State Change"
  ],
  "detail": {
    "clusterArn": [
      "arn:aws:ecs:ap-northeast-1:0000000000000:cluster/run_test_cluster"
    ],
    "lastStatus": [
      "STOPPED"
    ],
    "stoppedReason": [
      "Essential container in task exited"
    ],
    "containers": {
      "name": [
        "run-test"
      ],
      "exitCode": [
        1
      ]
    }
  }
}

これで要件が満たせた:v:

と思いきや

上記のEventパターンだと以下の条件で動作する

  • containers -> name -> “ldat” が存在する
    • かつ
  • containers -> exitCode -> 1が存在すれば 動く

つまり以下のようなイベントでも実行されてしまう。。

run-testコンテナは正常終了し、selenium-hubコンテナが異常終了(終了ステータス1)でも発火する
# 一部省略
"containers": [
  {
    "name": "run-test",
    "exitCode": 0,
  },
  {
    "name": "selenium-hub",
    "exitCode": 1,
  },

ジャーニーは続く・・・。
「こうすれば条件限定できるよ」みたいな情報をお待ちしております。

Tips: EventオブジェクトがEventパターンにマッチするか検証する

ECSタスク実行して動作確認をしてましたが、確認にめちゃめちゃ時間がかかります。
途中で知ったのがaws events test-event-patternで検証できるということでした。

AWS CLI Command Reference - test-event-pattern

引数
  test-event-pattern
--event-pattern <value>
--event <value>

引数--event-patternには先程のEventパターンを渡してあげます。
ただし、"はエスケープし、改行文字を削除する必要があります。
vimで:%s/"/\\"/gして、全選択shift + jすると割と楽です。

--event-patternの例
"{ \"source\": [ \"aws.ecs\" ], \"detail-type\": [ \"ECS Task State Change\" ], \"detail\": { \"clusterArn\": [ \"arn:aws:ecs:ap-northeast-1:0000000000000:cluster/run_test_cluster\" ], \"lastStatus\": [ \"STOPPED\" ], \"stoppedReason\": [ \"Essential container in task exited\" ], \"containers\": { \"name\": [ \"run-test\" ], \"exitCode\": [ 1 ] } } }"

引数--eventはLambdaなどでEvent内容を取得する必要があります。

以下のようにログに吐き出すことができます。

def lambda_handler(event, context):
    logger.info("Event: " + str(event))
サンプル
{
  "version": "0",
  "id": "00000000-11aa-22bb-33cc-456789def",
  "detail-type": "ECS Task State Change",
  "source": "aws.ecs",
  "account": "0000000000000000",
  "time": "2019-12-06T00:00:00Z",
  "region": "us-east-1",
  "resources": [
    "arn:aws:ecs:ap-northeast-1:0000000000000:task/00000000-11aa-22bb-33cc-456789def"
  ],
  "detail": {
    "clusterArn": "arn:aws:ecs:ap-northeast-1:0000000000000:cluster/run_test_cluster",
    "containerInstanceArn": "arn:aws:ecs:ap-northeast-1:0000000000000:container-instance/00000000-11aa-22bb-33cc-456789def",
    "containers": [
      {
        "containerArn": "arn:aws:ecs:ap-northeast-11:0000000000000:container/00000000-11aa-22bb-33cc-456789def",
        "exitCode": 1,
        "lastStatus": "STOPPED",
        "name": "run-test",
        "taskArn": "arn:aws:ecs:ap-northeast-1:0000000000000:task/00000000-11aa-22bb-33cc-456789def"
      },
      {
        "containerArn": "arn:aws:ecs:ap-northeast-1:0000000000000:container/00000000-11aa-22bb-33cc-456789def",
        "exitCode": 142,
        "lastStatus": "STOPPED",
        "name": "selenium-node-chrome",
        "taskArn": "arn:aws:ecs:ap-northeast-1:0000000000000:task/00000000-11aa-22bb-33cc-456789def"
      }
    ],
    "stoppedReason": "Essential container in task exited",
    "createdAt": "2019-12-06T00:00:00.000Z",
    "desiredStatus": "STOPPED",
    "group": "family:run-test-task",
    "lastStatus": "STOPPED",
    "overrides": {},
    "startedAt": "2019-12-06T00:00:00.000Z",
    "startedBy": "ecs-svc/00000000000000",
    "updatedAt": "2019-12-06T00:00:00.000Z",
    "taskArn": "arn:aws:ecs:ap-northeast-1:0000000000000:task/00000000-11aa-22bb-33cc-456789def",
    "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:0000000000000:task-definition/run-test-task:2",
    "version": 4
  }
}

更に"はエスケープし、改行文字を削除する必要があります

"{ \"version\": \"0\", \"id\": \"00000000-11aa-22bb-33cc-456789def\", \"detail-type\": \"ECS Task State Change\", \"source\": \"aws.ecs\", \"account\": \"0000000000000000\", \"time\": \"2019-12-06T00:00:00Z\", \"region\": \"us-east-1\", \"resources\": [ \"arn:aws:ecs:ap-northeast-1:0000000000000:task/00000000-11aa-22bb-33cc-456789def\" ], \"detail\": { \"clusterArn\": \"arn:aws:ecs:ap-northeast-1:0000000000000:cluster/run_test_cluster\", \"containerInstanceArn\": \"arn:aws:ecs:ap-northeast-1:0000000000000:container-instance/00000000-11aa-22bb-33cc-456789def\", \"containers\": [ { \"containerArn\": \"arn:aws:ecs:ap-northeast-11:0000000000000:container/00000000-11aa-22bb-33cc-456789def\", \"exitCode\": 1, \"lastStatus\": \"STOPPED\", \"name\": \"run-test\", \"taskArn\": \"arn:aws:ecs:ap-northeast-1:0000000000000:task/00000000-11aa-22bb-33cc-456789def\" }, { \"containerArn\": \"arn:aws:ecs:ap-northeast-1:0000000000000:container/00000000-11aa-22bb-33cc-456789def\", \"exitCode\": 142, \"lastStatus\": \"STOPPED\", \"name\": \"selenium-node-chrome\", \"taskArn\": \"arn:aws:ecs:ap-northeast-1:0000000000000:task/00000000-11aa-22bb-33cc-456789def\" } ], \"stoppedReason\": \"Essential container in task exited\", \"createdAt\": \"2019-12-06T00:00:00.000Z\", \"desiredStatus\": \"STOPPED\", \"group\": \"family:run-test-task\", \"lastStatus\": \"STOPPED\", \"overrides\": {}, \"startedAt\": \"2019-12-06T00:00:00.000Z\", \"startedBy\": \"ecs-svc/00000000000000\", \"updatedAt\": \"2019-12-06T00:00:00.000Z\", \"taskArn\": \"arn:aws:ecs:ap-northeast-1:0000000000000:task/00000000-11aa-22bb-33cc-456789def\", \"taskDefinitionArn\": \"arn:aws:ecs:ap-northeast-1:0000000000000:task-definition/run-test-task:2\", \"version\": 4 } }"

組み合わせると以下のようなコマンドになります。

$ aws events test-event-pattern \
 --region ap-northeast-1 \
 --event-pattern "{ \"source\": [ \"aws.ecs\" ], \"detail-type\": [ \"ECS Task State Change\" ], \"detail\": { \"clusterArn\": [ \"arn:aws:ecs:ap-northeast-1:0000000000000:cluster/run_test_cluster\" ], \"lastStatus\": [ \"STOPPED\" ], \"stoppedReason\": [ \"Essential container in task exited\" ], \"containers\": { \"name\": [ \"run-test\" ], \"exitCode\": [ 1 ] } } }" \
 --event "{ \"version\": \"0\", \"id\": \"00000000-11aa-22bb-33cc-456789def\", \"detail-type\": \"ECS Task State Change\", \"source\": \"aws.ecs\", \"account\": \"0000000000000000\", \"time\": \"2019-12-06T00:00:00Z\", \"region\": \"us-east-1\", \"resources\": [ \"arn:aws:ecs:ap-northeast-1:0000000000000:task/00000000-11aa-22bb-33cc-456789def\" ], \"detail\": { \"clusterArn\": \"arn:aws:ecs:ap-northeast-1:0000000000000:cluster/run_test_cluster\", \"containerInstanceArn\": \"arn:aws:ecs:ap-northeast-1:0000000000000:container-instance/00000000-11aa-22bb-33cc-456789def\", \"containers\": [ { \"containerArn\": \"arn:aws:ecs:ap-northeast-11:0000000000000:container/00000000-11aa-22bb-33cc-456789def\", \"exitCode\": 1, \"lastStatus\": \"STOPPED\", \"name\": \"run-test\", \"taskArn\": \"arn:aws:ecs:ap-northeast-1:0000000000000:task/00000000-11aa-22bb-33cc-456789def\" }, { \"containerArn\": \"arn:aws:ecs:ap-northeast-1:0000000000000:container/00000000-11aa-22bb-33cc-456789def\", \"exitCode\": 142, \"lastStatus\": \"STOPPED\", \"name\": \"selenium-node-chrome\", \"taskArn\": \"arn:aws:ecs:ap-northeast-1:0000000000000:task/00000000-11aa-22bb-33cc-456789def\" } ], \"stoppedReason\": \"Essential container in task exited\", \"createdAt\": \"2019-12-06T00:00:00.000Z\", \"desiredStatus\": \"STOPPED\", \"group\": \"family:run-test-task\", \"lastStatus\": \"STOPPED\", \"overrides\": {}, \"startedAt\": \"2019-12-06T00:00:00.000Z\", \"startedBy\": \"ecs-svc/00000000000000\", \"updatedAt\": \"2019-12-06T00:00:00.000Z\", \"taskArn\": \"arn:aws:ecs:ap-northeast-1:0000000000000:task/00000000-11aa-22bb-33cc-456789def\", \"taskDefinitionArn\": \"arn:aws:ecs:ap-northeast-1:0000000000000:task-definition/run-test-task:2\", \"version\": 4 } }"

{
    "Result": true
}
28
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
17