LIFULLでSETとしてテスト自動化やCI/CDの改善をやってます。
弊チームの自動テストはECSタスクで実行しています。
その自動テストの運用改善の一環で調べたことを書き綴ります。
自動システムテストのアーキテクチャ
下記の流れでテスト実行・テスト終了通知を行っていました。
Scheduled task(定時実行)
↓
ECS Run task(自動テスト実行)
↓
Cloudwatch Rules(ECS Taskの完了を検知)
↓
Lambda(チャットツールへ通知)
要求・要件
従来のままだとテストがすべてPassしても通知が来てしまいました。
ある自動テストプロジェクトの際に「テストがFailしたときだけチャットに通知させたい」という要求が生まれました。
よく考えれば当たり前の要求です。(たまたま私達の運用ではいらなかった)
要件に落とし込むと
「ECS Run taskで実行したテスト実行コンテナの終了ステータスが異常値の場合だけLambdaを発火させたい」
となりました。
やったこと①:ECSイベントを調査
ECSタスクはある状態から別の状態に切り替わるときにイベントが発生します。
イベントには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
の中にあるexitCode
とname
が使えそうなことがわかりました。
#やったこと②: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のときに動く
先程のexitCode
とname
を条件に追加したものです。
{
"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パターンだと以下の条件で動作する
- containers -> name -> “ldat” が存在する
- かつ
- containers -> exitCode -> 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
すると割と楽です。
"{ \"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
}