SQSをポーリングするECS構成でタスクスケールイン保護を試してみた

SQSをポーリングするECS構成でタスクスケールイン保護を試してみた

2025.10.12

はじめに

バッチの実行に同時実行数を制御しながらECSタスクを使うケースがあります。Lambdaと比較して例えば以下のような要件がある場合です。

  • 15分以上かかるタスク
  • 10GBより大きいディスクが必要かつコスト面などでEFSを利用したくない

今回はECSタスクがSQSをポーリングする構成を試してみました。他にもいろんなパターンがあると思います。[1]

本構成はECSタスクが1つだとメッセージが多い場合、要求される時間内に処理出来ないケースがありAuto Scalingとセットで設計することが多いと思います。構成は以下の通りです。

architecture.drawio

そこで懸念になるのがタスク実行時のスケールインで実行中の処理が中断されてしまうことです。以下のようなタイミングでスケールインが発生します。

  • オートスケール時
  • ローリングアップデート時

可視性タイムアウト時間経過後再キューイングされますが、すでに処理した時間分と可視性タイムアウトまでの発生するため、律速要因となります。

ECSはGraceful shutdownができるようにSIGTERMをプロセスに送信し、指定したstopTimeout時間(最大120秒)後にSIGKILLを送信します。その間に可視性タイムアウトを短くするなど対応は取れますが、SQSのメッセージに応じた処理を終えてからスケールイン(停止するのが)分かりやすいですし、処理時間的にもリソース的にも効率が良いです。

今回はECSのスケールイン保護機能を利用して上記の問題に対応してみます。

https://aws.amazon.com/jp/blogs/news/announcing-amazon-ecs-task-scale-in-protection/

ソースコード

ソースはこちらです。下記をベースに主要なところをピックアップして説明します。

https://github.com/shuntaka9576/sqs-ecs-polling-worker

インフラ実装

はじめに

今回は以下の構成です。CloudFormationのスタックは2つでCDKで作成します。

  • VPC, ECS Clusterスタック
  • ECS Auto Scalingスタック

ECのサービスとECSタスクは ecspresso で作成します。Auto Scalingでスタックを分けたのは、ECSサービスとECSタスクを ecspresso に切り出しているためです。

VPC, ECS Serviceスタック

VPCとECSでConstructを分けています
https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/iac/lib/stacks/async-worker-stack.ts#L1-L24

VPCはこちら
https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/iac/lib/constructs/vpc-construct.ts#L1-L44

ECSはこちら
https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/iac/lib/constructs/async-worker-construct.ts#L1-L125

ecspressoから参照できるように、SSMパラメータストアに値を登録しています。
https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/iac/lib/constructs/async-worker-construct.ts#L88-L123

ECS ServiceとTask定義

packages/iac/ecspressoに格納しています。

今回はPROCESSING_SLEEP_DURATION_MSという値を使って、処理時間をシミュレーションします。

https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/iac/ecspresso/ecs-task-def.jsonnet#L10-L27

後述しますが、AutoScaling設定ではECSタスクが最大3までスケールするようになっていますので、desiredCount=3でmaximumPercent=200を設定しています。これにより最大スケール数の状態(3タスク稼働中)でデプロイしても、一時的に最大6タスク(既存3 + 新3)まで起動できるため、サービスを停止することなくローリングデプロイが可能になります。メッセージが0の場合はECSタスクは0までスケールインします。

https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/iac/ecspresso/ecs-service-def.jsonnet#L23-L25
https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/iac/ecspresso/ecs-service-def.jsonnet#L16

AutoScalingスタック

AutoScalingの設定は以下です。

https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/iac/lib/stacks/worker-autoscaling-stack.ts#L1-L101

スケールの設定はSQSの可視ではないメッセージを示すApproximateNumberOfMessagesVisibleの数を元に、ECSタスク数を制御しています。

https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/iac/lib/stacks/worker-autoscaling-stack.ts#L86-L87

前述のメッセージ数とECSタスク数の関係は以下の通りです。
https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/iac/lib/stacks/worker-autoscaling-stack.ts#L58-L76

アプリケーション実装

シーケンス

シーケンスは以下の通りです。

  • SQSのメッセージを取得し、処理を実行、失敗した場合はメッセージを削除しエラーログ出力。運用で再実行と仮定(=処理失敗時の処理は分量の都合上割愛します🙇‍♂️)
  • RDBの処理は冪等性を担保するため(データベース実装も同様に今回割愛します🙇‍♂️)

実装

アプリの実装はpackages/workerに格納しています。

メインループまでの処理

mainがエントリポイントで、まずECS Taskのメタデータエンドポイントから、ECSタスクに一意なARNを取得します。ECSタスク保護をする際に必要な値となります。ログに付与しているのは、スケールした際に非常に追い辛くなるためです。これはOTelなど別のアプローチがありますが、今回は簡易にloggerで実装します。

https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/worker/src/index.ts#L11-L24

メタデータエンドポイント処理
https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/worker/src/clients/ecsClient.ts#L19-L28

メインループです。ループごとにUUIDを生成して、loggerに入れています。これも前述と同じ理由です。
https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/worker/src/index.ts#L11-L52

SIGTERMを受け取ったら、ループが終わって正常終了するようになっています。
https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/worker/src/index.ts#L54-L58

ecs/svcはSIGTERM送信後stopTimeout後にSIGKILLが送信されます。最大120秒のため、ECSタスク実行中で送信されるとstopTimeout後にSIGKILLが送信され、強制的に終了します。次の項で終了しないようにECSタスク保護の処理を説明します。

タスク保護からSQSメッセージ取得

メッセージの処理は以下です。
https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/worker/src/index.ts#L28-L33

SQSからメッセージを取得する前にタスクの保護をします。これはスケールイン時、タスク保護をしようとすると保護できない理由が検出できるためです。

https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/worker/src/worker/processor.ts#L22-L35

具体的な処理は以下です。SDK上例外は投げられないため、failuresの数をカウントして0より大きい場合、例外を投げるようにします。

https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/worker/src/clients/ecsClient.ts#L30-L52

エラーは補足して、ループを終了させて、正常終了(exit 0)します。
https://github.com/shuntaka9576/sqs-ecs-polling-worker/blob/b1eb6601f31b4930545cd9b98883b69fbac34afe/packages/worker/src/worker/processor.ts#L59-L67

環境構築

VPC, ECS Serviceスタックの構築

READMEに従って環境を構築します。

VPC ECS Clusterスタック作成コマンド
			
			cd packages/iac
pnpm cdk deploy \
  sqs-ecs-worker-stack \
  --require-approval never

		
出力結果
			
			[WARNING] aws-cdk-lib.aws_ecr.RepositoryProps#autoDeleteImages is deprecated.
  Use `emptyOnDelete` instead.
  This API will be removed in the next major release.

✨  Synthesis time: 0.81s

sqs-ecs-worker-stack: deploying... [1/1]
sqs-ecs-worker-stack: creating CloudFormation changeset...

 ✅  sqs-ecs-worker-stack

✨  Deployment time: 53.29s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:111111111111:stack/sqs-ecs-worker-stack/d6dc9550-a709-11f0-935b-0ab77681b2c7

✨  Total time: 54.1s

NOTICES         (What's this? https://github.com/aws/aws-cdk/wiki/CLI-Notices)

31885   (cli): Bootstrap stack outdated

        Overview: The bootstrap stack in aws://111111111111/ap-northeast-1 is outdated.
                  We recommend at least version 21, distributed with CDK CLI
                  2.149.0 or higher. Please rebootstrap your environment by
                  running 'cdk bootstrap aws://111111111111/ap-northeast-1'

        Affected versions: bootstrap: <21

        More information at: https://github.com/aws/aws-cdk/issues/31885

If you don’t want to see a notice anymore, use "cdk acknowledge <id>". For example, "cdk acknowledge 31885".

		
コンテナをECRにpush
			
			cd ../../
ECR_REPOSITORY_URI=$(aws ssm get-parameter \
  --name /sqs-ecs-worker/worker/ecr-repository-uri \
  --query 'Parameter.Value' \
  --output text)

GIT_HASH=$(git rev-parse --short HEAD)

aws ecr get-login-password --region ap-northeast-1 | \
  docker login --username AWS --password-stdin ${ECR_REPOSITORY_URI}

docker build -t ${ECR_REPOSITORY_URI}:${GIT_HASH} .

docker push ${ECR_REPOSITORY_URI}:${GIT_HASH}

		
出力結果
			
			Login Succeeded
[+] Building 0.7s (24/24) FINISHED                                                                                                                                               docker:rancher-desktop
 => [internal] load build definition from Dockerfile                                                                                                                                               0.0s
 => => transferring dockerfile: 1.02kB                                                                                                                                                             0.0s
 => [internal] load metadata for docker.io/library/node:24.4.1-slim                                                                                                                                0.0s
 => [internal] load metadata for gcr.io/distroless/nodejs24-debian12:latest                                                                                                                        0.6s
 => [internal] load .dockerignore                                                                                                                                                                  0.0s
 => => transferring context: 119B                                                                                                                                                                  0.0s
 => [worker 1/4] FROM gcr.io/distroless/nodejs24-debian12:latest@sha256:1bc10dd5bef59356cde4a557052e6917dbd704d5632bdec6b80f63900fc3b53c                                                           0.0s
 => [base  1/10] FROM docker.io/library/node:24.4.1-slim                                                                                                                                           0.0s
 => [internal] load build context                                                                                                                                                                  0.0s
 => => transferring context: 3.00kB                                                                                                                                                                0.0s
 => CACHED [worker 2/4] WORKDIR /app                                                                                                                                                               0.0s
 => CACHED [base  2/10] RUN npm install -g pnpm                                                                                                                                                    0.0s
 => CACHED [base  3/10] WORKDIR /usr/src/app                                                                                                                                                       0.0s
 => CACHED [base  4/10] COPY pnpm-lock.yaml ./                                                                                                                                                     0.0s
 => CACHED [base  5/10] RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store     pnpm fetch --frozen-lockfile                                                                                   0.0s
 => CACHED [base  6/10] COPY package.json pnpm-workspace.yaml .npmrc ./                                                                                                                            0.0s
 => CACHED [base  7/10] COPY packages/worker/package.json packages/worker/                                                                                                                         0.0s
 => CACHED [base  8/10] RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store     pnpm install --frozen-lockfile --offline                                                                       0.0s
 => CACHED [base  9/10] COPY packages/worker/ packages/worker/                                                                                                                                     0.0s
 => CACHED [base 10/10] RUN pnpm install --frozen-lockfile                                                                                                                                         0.0s
 => CACHED [build 1/2] WORKDIR /usr/src/app/packages/worker                                                                                                                                        0.0s
 => CACHED [build 2/2] RUN pnpm build                                                                                                                                                              0.0s
 => CACHED [worker 3/4] COPY --from=build /usr/src/app/packages/worker/dist .                                                                                                                      0.0s
 => CACHED [pruned 1/2] WORKDIR /usr/src/app                                                                                                                                                       0.0s
 => CACHED [pruned 2/2] RUN pnpm --filter worker --prod deploy pruned                                                                                                                              0.0s
 => CACHED [worker 4/4] COPY --from=pruned /usr/src/app/pruned/node_modules ./node_modules                                                                                                         0.0s
 => exporting to image                                                                                                                                                                             0.0s
 => => exporting layers                                                                                                                                                                            0.0s
 => => writing image sha256:84f84d6d172b28c49107cdfef325e65e07eb0603744a400561e17008578811a7                                                                                                       0.0s
 => => naming to 111111111111.dkr.ecr.ap-northeast-1.amazonaws.com/sqs-ecs-worker:29a6ae2                                                                                                          0.0s
The push refers to repository [111111111111.dkr.ecr.ap-northeast-1.amazonaws.com/sqs-ecs-worker]
264e4a31a93f: Pushed
265df0e9ffc8: Pushed
4036cc6d2a90: Pushed
db10e52237b1: Pushed
cda8aa10c7ef: Pushed
e84030d2f270: Pushed
245157cfc419: Pushed
15058730e914: Pushed
13284cbc15c4: Pushed
304542718315: Pushed
bfe9137a1b04: Pushed
f4aee9e53c42: Pushed
1a73b54f556b: Pushed
2a92d6ac9e4f: Pushed
bbb6cacb8c82: Pushed
6f1cdceb6a31: Pushed
af5aa97ebe6c: Pushed
4d049f83d9cf: Pushed
114dde0fefeb: Pushed
4840c7c54023: Pushed
8fa10c0194df: Pushed
378b3db0974f: Pushed
29a6ae2: digest: sha256:c90bb978b353f1dc3f327e704e500976280f50e4a5196e800920eabc7a99580e size: 4908

		

ecspressoでサービス、タスクの作成

ecspressoでECSサービス、タスクをデプロイ
			
			cd packages/iac/ecspresso

export IMAGE_TAG=$GIT_HASH
ecspresso diff # 確認
ecspresso deploy

		
出力
			
			2025-10-12T10:24:13.641+09:00 [INFO] ecspresso version: v2.6.0
2025-10-12T10:24:13.999+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] service not found, will create a new service
---
+++ ecs-service-def.jsonnet
@@ -1 +1,38 @@
+{
+  "availabilityZoneRebalancing": "DISABLED",
+  "deploymentConfiguration": {
+    "alarms": {
+      "enable": false,
+      "rollback": false
+    },
+    "bakeTimeInMinutes": 0,
+    "deploymentCircuitBreaker": {
+      "enable": true,
+      "rollback": true
+    },
+    "maximumPercent": 200,
+    "minimumHealthyPercent": 0,
+    "strategy": "ROLLING"
+  },
+  "desiredCount": 3,
+  "enableECSManagedTags": false,
+  "enableExecuteCommand": true,
+  "forceNewDeployment": false,
+  "networkConfiguration": {
+    "awsvpcConfiguration": {
+      "assignPublicIp": "ENABLED",
+      "securityGroups": [
+        "sg-019beb6ddb5d88b87"
+      ],
+      "subnets": [
+        "subnet-0440d2107c28b4ebc",
+        "subnet-0926c51e4abda2d06"
+      ]
+    }
+  },
+  "platformVersion": "LATEST",
+  "propagateTags": "NONE",
+  "serviceConnectConfiguration": {
+    "enabled": false
+  }
+}

--- arn:aws:ecs:ap-northeast-1:111111111111:task-definition/sqs-ecs-worker:7
+++ ecs-task-def.jsonnet
@@ -22,7 +22,7 @@
         }
       ],
       "essential": true,
-      "image": "111111111111.dkr.ecr.ap-northeast-1.amazonaws.com/sqs-ecs-worker:b1eb660",
+      "image": "111111111111.dkr.ecr.ap-northeast-1.amazonaws.com/sqs-ecs-worker:29a6ae2",
       "logConfiguration": {
         "logDriver": "awslogs",
         "options": {

2025-10-12T10:24:14.433+09:00 [INFO] ecspresso version: v2.6.0
2025-10-12T10:24:14.434+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] Starting deploy
2025-10-12T10:24:14.560+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] Service sqs-ecs-worker not found. Creating a new service
2025-10-12T10:24:14.560+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] Starting create service
2025-10-12T10:24:14.968+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] Registering a new task definition...
2025-10-12T10:24:15.036+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] Task definition is registered sqs-ecs-worker:8
2025-10-12T10:24:15.727+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] Service is created
2025-10-12T10:24:18.802+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] Waiting for service stable...(it will take a few minutes)
2025-10-12T10:24:28.951+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker]  PRIMARY sqs-ecs-worker:8 desired:0 pending:0 running:0 IN_PROGRESS(ECS deployment ecs-svc/3215064031745877644 in progress.)
2025-10-12T10:24:31.154+09:00 (service sqs-ecs-worker) has started 1 tasks: (task 2a25d332712e4c36a8234db1ae346986).
2025-10-12T10:24:38.874+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker]  PRIMARY sqs-ecs-worker:8 desired:1 pending:1 running:0 IN_PROGRESS(ECS deployment ecs-svc/3215064031745877644 in progress.)
2025-10-12T10:24:48.947+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker]  PRIMARY sqs-ecs-worker:8 desired:1 pending:0 running:1 IN_PROGRESS(ECS deployment ecs-svc/3215064031745877644 in progress.)
2025-10-12T10:24:50.250+09:00 (service sqs-ecs-worker) has started 2 tasks: (task c377b51dc7904b058567434464cea4e0) (task db946305b6b64871b7c8f77bbf4af6a3).
2025-10-12T10:24:58.955+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker]  PRIMARY sqs-ecs-worker:8 desired:3 pending:2 running:1 IN_PROGRESS(ECS deployment ecs-svc/3215064031745877644 in progress.)
2025-10-12T10:25:08.936+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker]  PRIMARY sqs-ecs-worker:8 desired:3 pending:0 running:3 IN_PROGRESS(ECS deployment ecs-svc/3215064031745877644 in progress.)
2025-10-12T10:25:22.271+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] Service is stable now. Completed!

		

desiredの設定値通りECSタスクが3つ起動していることがわかります。まだAutoScalingの設定を入れていないので3つのタスクが常時起動します。

CleanShot 2025-10-12 at 10.25.31@2x

CloudWatchのログからも確認できます。

			
			-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|   timestamp   |                                                                                    message                                                                                     |                     logStreamName                      |
|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
| 1760232286694 | {"level":30,"time":1760232286694,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/2a25d332712e4c36a8234db1ae346986","msg":"Starting SQS polling worker"} | sqs-ecs-worker/worker/2a25d332712e4c36a8234db1ae346986 |
| 1760232306309 | {"level":30,"time":1760232306308,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/db946305b6b64871b7c8f77bbf4af6a3","msg":"Starting SQS polling worker"} | sqs-ecs-worker/worker/db946305b6b64871b7c8f77bbf4af6a3 |
| 1760232307149 | {"level":30,"time":1760232307148,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/c377b51dc7904b058567434464cea4e0","msg":"Starting SQS polling worker"} | sqs-ecs-worker/worker/c377b51dc7904b058567434464cea4e0 |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

		

AutoScalingスタックのデプロイ

			
			cd ../
pnpm cdk deploy \
  sqs-ecs-worker-autoscaling-stack \
  --require-approval never

		
出力
			
			[WARNING] aws-cdk-lib.aws_ecr.RepositoryProps#autoDeleteImages is deprecated.
  Use `emptyOnDelete` instead.
  This API will be removed in the next major release.

✨  Synthesis time: 0.77s

sqs-ecs-worker-autoscaling-stack: start: Building sqs-ecs-worker-autoscaling-stack Template
sqs-ecs-worker-autoscaling-stack: success: Built sqs-ecs-worker-autoscaling-stack Template
sqs-ecs-worker-autoscaling-stack: start: Publishing sqs-ecs-worker-autoscaling-stack Template (current_account-current_region-82b6a1aa)
sqs-ecs-worker-autoscaling-stack: success: Published sqs-ecs-worker-autoscaling-stack Template (current_account-current_region-82b6a1aa)
sqs-ecs-worker-autoscaling-stack: deploying... [1/1]
sqs-ecs-worker-autoscaling-stack: creating CloudFormation changeset...

 ✅  sqs-ecs-worker-autoscaling-stack

✨  Deployment time: 48.17s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:111111111111:stack/sqs-ecs-worker-autoscaling-stack/8dd9b7a0-a70b-11f0-96f3-0ea8106d25c1

✨  Total time: 48.93s

NOTICES         (What's this? https://github.com/aws/aws-cdk/wiki/CLI-Notices)

31885   (cli): Bootstrap stack outdated

        Overview: The bootstrap stack in aws://111111111111/ap-northeast-1 is outdated.
                  We recommend at least version 21, distributed with CDK CLI
                  2.149.0 or higher. Please rebootstrap your environment by
                  running 'cdk bootstrap aws://111111111111/ap-northeast-1'

        Affected versions: bootstrap: <21

        More information at: https://github.com/aws/aws-cdk/issues/31885

If you don’t want to see a notice anymore, use "cdk acknowledge <id>". For example, "cdk acknowledge 31885".

		

デプロイ後CloudWatchアラームが検知して、0にスケールされました。
CleanShot 2025-10-12 at 10.37.22@2x

ECSサービスのイベントからも停止が確認できます。
CleanShot 2025-10-12 at 10.37.39@2x

上記のイベントにもありますが、SQSのループ後タスク保護を解除し、またタスク保護するタイミングでDEPLOYMENT_BLOCKEDエラーを拾ってタスクを正常終了(exit 0)しています。

			
			--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|   timestamp   |                                                                                                                                   message                                                                                                                                   |                     logStreamName                      |
|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
| 1760232286694 | {"level":30,"time":1760232286694,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/2a25d332712e4c36a8234db1ae346986","msg":"Starting SQS polling worker"}                                                                                              | sqs-ecs-worker/worker/2a25d332712e4c36a8234db1ae346986 |
| 1760232306309 | {"level":30,"time":1760232306308,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/db946305b6b64871b7c8f77bbf4af6a3","msg":"Starting SQS polling worker"}                                                                                              | sqs-ecs-worker/worker/db946305b6b64871b7c8f77bbf4af6a3 |
| 1760232307149 | {"level":30,"time":1760232307148,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/c377b51dc7904b058567434464cea4e0","msg":"Starting SQS polling worker"}                                                                                              | sqs-ecs-worker/worker/c377b51dc7904b058567434464cea4e0 |
| 1760232969138 | {"level":30,"time":1760232969138,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/db946305b6b64871b7c8f77bbf4af6a3","loopId":"d09a13fa-bd5a-4c3e-b939-e7498426bd3e","reason":"DEPLOYMENT_BLOCKED","msg":"Task protection failed, exiting gracefully"} | sqs-ecs-worker/worker/db946305b6b64871b7c8f77bbf4af6a3 |
| 1760232969138 | {"level":30,"time":1760232969138,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/db946305b6b64871b7c8f77bbf4af6a3","msg":"Polling stopped, worker shutting down"}                                                                                    | sqs-ecs-worker/worker/db946305b6b64871b7c8f77bbf4af6a3 |
| 1760232969827 | {"level":30,"time":1760232969827,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/2a25d332712e4c36a8234db1ae346986","loopId":"91880224-1805-43b3-a7e4-3d3b14270667","reason":"DEPLOYMENT_BLOCKED","msg":"Task protection failed, exiting gracefully"} | sqs-ecs-worker/worker/2a25d332712e4c36a8234db1ae346986 |
| 1760232969828 | {"level":30,"time":1760232969827,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/2a25d332712e4c36a8234db1ae346986","msg":"Polling stopped, worker shutting down"}                                                                                    | sqs-ecs-worker/worker/2a25d332712e4c36a8234db1ae346986 |
| 1760232970155 | {"level":30,"time":1760232970155,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/c377b51dc7904b058567434464cea4e0","loopId":"ed88e1e3-53b2-43be-a0e0-e98d967561fa","reason":"DEPLOYMENT_BLOCKED","msg":"Task protection failed, exiting gracefully"} | sqs-ecs-worker/worker/c377b51dc7904b058567434464cea4e0 |
| 1760232970155 | {"level":30,"time":1760232970155,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/c377b51dc7904b058567434464cea4e0","msg":"Polling stopped, worker shutting down"}                                                                                    | sqs-ecs-worker/worker/c377b51dc7904b058567434464cea4e0 |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

		

スケールアウトの確認

タスク数が0になったので、SQSにメッセージを5つ投入します。

			
			QUEUE_URL=$(aws ssm get-parameter \
  --name /sqs-ecs-worker/worker/queue-url \
  --query 'Parameter.Value' \
  --output text)

date && seq 1 5 | xargs -P 5 -I {} aws sqs send-message --queue-url $QUEUE_URL --message-body "{\"type\": \"test\", \"id\": \"msg_{}\"}" && date

		
出力
			
			Sun Oct 12 10:53:11 JST 2025
{
    "MD5OfMessageBody": "cf69a0029a10ccd213b242fc02b7d7f0",
    "MessageId": "3bdcc23e-e3c1-4eb8-aa9c-7f65fb846e48"
}
{
    "MD5OfMessageBody": "bc3ef5071d583c2fdbea27fd8d095fca",
    "MessageId": "a20d88e6-18bc-43ba-9e1f-79c8cc5fdc6e"
}
{
    "MD5OfMessageBody": "e7de5c07eac255829ef765dd1b27c05c",
    "MessageId": "759f8b19-f1b8-4a9e-a576-afc089057ee7"
}
{
    "MD5OfMessageBody": "d0b8f7dac89e9cfcd8334aa4a8e0cf0a",
    "MessageId": "8df8f3ca-4c62-46d0-a5f1-794143360209"
}
{
    "MD5OfMessageBody": "7f920d8f057d1f229a8591c2d6e77cb2",
    "MessageId": "f8865fd8-8554-48af-9dec-2dd6f26075bc"
}
Sun Oct 12 10:53:11 JST 2025

		

SQSにメッセージがキューイングされていることを確認します。
CleanShot 2025-10-12 at 10.54.02@2x

スケールアウトが動作しました。画像は保留中ですが、すぐに実行中になりました。
CleanShot 2025-10-12 at 10.56.41@2x

スケールアウトした結果、ECSタスクが3つ立ち上がり、SQSからメッセージを3つ取得したため、ApproximateNumberOfMessagesVisibleが2(=5-3)なりました。結果スケールインが走っていますが、タスク保護が働いているためスケールインはされません。
CleanShot 2025-10-12 at 11.06.40@2x
CleanShot 2025-10-12 at 11.00.30@2x

一旦5つ完了するまで待ちます(10分) (ちなみに可視性タイムアウトは20分を設定しています)。イベント結果は以下の通りです。

CleanShot 2025-10-12 at 11.36.26@2x

赤枠部分はSQSの最初の3つのメッセージを処理後で、2つのタスクを停止し、同時に1つのタスクが始まっています。以下の3つタスクがタスク保護エンドポイントを叩いてDEPLOYMENT_BLOCKEDで3つとも正常終了(exit 0)してしまったため、1つのタスクが起動しています。

  1. c51c10a35cfe47fd9cf31fb45b20dbd8
  2. 8fda4fe1acfa4a61a3334c9e6e48a046
  3. 5323aacb4e2f4a22973c5695d63e7a88

AutoSaclingの挙動として3つのタスクのうち上記の1と2を停止する予定でした。なので保護解除後、SIGTERMを送信されるはずが、アプリケーション実装の都合上先に落ちます。3はECSのサービスの的に意図していないのか(?)正常終了(exit 0)しているが、停止済み(黄色)でEssential container in task exitedになっています。exit 1以上の場合、エラー(赤文字)表示されます。

CleanShot 2025-10-12 at 11.41.09@2x

推測ですが以下のような処理挙動かなと思います

  1. AutoSaclingでECSタスク数が1に設定されたため、保護可能なタスクが1つになる
  2. 3つのタスクはほぼ同時に終了し、再度保護をかけたため、DEPLOYMENT_BLOCKEDで全てのタスクが正常終了
  3. ecs/svcはスケールイン対象でない必須のタスクまで正常終了したため、Essential container in task exited(exit 0)として表示

fujiwara/tap/tracerでログを追ってみると、exit code 0 なので問題はありません

			
			$ tracer sqs-ecs-worker 5323aacb4e2f4a22973c5695d63e7a88
zsh: correct 'tracer' to 'trace' [nyae]? n
Tracer: 5323aacb4e2f4a22973c5695d63e7a88 on sqs-ecs-worker
2025-10-12T10:56:07.169+09:00   TASK    Created
2025-10-12T10:56:07.342+09:00   SERVICE (service sqs-ecs-worker) has started 3 tasks: (task 5323aacb4e2f4a22973c5695d63e7a88) (task 8fda4fe1acfa4a61a3334c9e6e48a046) (task c51c10a35cfe47fd9cf31fb45b20dbd8).
2025-10-12T10:56:10.795+09:00   TASK    Connected
2025-10-12T10:56:17.315+09:00   TASK    Pull started
2025-10-12T10:56:20.806+09:00   TASK    Pull stopped
2025-10-12T10:56:21.360+09:00   TASK    Started
2025-10-12T10:56:21.626+09:00   CONTAINER:worker        {"level":30,"time":1760234181626,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/5323aacb4e2f4a22973c5695d63e7a88","msg":"Starting SQS polling worker"}
2025-10-12T10:56:21.847+09:00   CONTAINER:worker        {"level":30,"time":1760234181847,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/5323aacb4e2f4a22973c5695d63e7a88","loopId":"f82bf648-b042-46d6-84f3-df9df3298b4a","messageBody":"{\"type\": \"test\", \"id\": \"msg_3\"}","msg":"Message received"}
2025-10-12T11:06:21.946+09:00   CONTAINER:worker        {"level":30,"time":1760234781946,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/5323aacb4e2f4a22973c5695d63e7a88","loopId":"f82bf648-b042-46d6-84f3-df9df3298b4a","msg":"Message processing completed"}
2025-10-12T11:06:21.979+09:00   CONTAINER:worker        {"level":30,"time":1760234781979,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/5323aacb4e2f4a22973c5695d63e7a88","loopId":"f82bf648-b042-46d6-84f3-df9df3298b4a","msg":"Message deleted from queue"}
2025-10-12T11:06:22.053+09:00   CONTAINER:worker        {"level":30,"time":1760234782053,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/5323aacb4e2f4a22973c5695d63e7a88","loopId":"709bd90a-f590-431d-be99-cfe4c62f9f87","reason":"DEPLOYMENT_BLOCKED","msg":"Task protection failed, exiting gracefully"}
2025-10-12T11:06:22.053+09:00   CONTAINER:worker        {"level":30,"time":1760234782053,"taskArn":"arn:aws:ecs:ap-northeast-1:111111111111:task/sqs-ecs-worker/5323aacb4e2f4a22973c5695d63e7a88","msg":"Polling stopped, worker shutting down"}
2025-10-12T11:06:22.144+09:00   TASK    Execution stopped
2025-10-12T11:06:29.646+09:00   SERVICE (service sqs-ecs-worker) has stopped 2 running tasks: (task c51c10a35cfe47fd9cf31fb45b20dbd8) (task 8fda4fe1acfa4a61a3334c9e6e48a046).
2025-10-12T11:06:32.177+09:00   TASK    Stopping
2025-10-12T11:06:32.177+09:00   TASK    StoppedReason:Essential container in task exited
2025-10-12T11:06:32.177+09:00   TASK    StoppedCode:EssentialContainerExited
2025-10-12T11:06:47.930+09:00   TASK    Stopped
2025-10-12T11:06:49.030+09:00   SERVICE (service sqs-ecs-worker) has started 1 tasks: (task b5533da3620a4e048083f15c58074be0).
2025-10-12T11:27:13.615+09:00   CONTAINER:worker        LastStatus:STOPPED HealthStatus:UNKNOWN (exit code: 0)
2025-10-12T11:27:13.615+09:00   TASK    LastStatus:STOPPED

		

残りのイベントは、1つのタスクが2つのメッセージを処理して、20分たって0スケールしています。

ローリングアップデートの確認

メッセージを10件投入します

			
			QUEUE_URL=$(aws ssm get-parameter \
  --name /sqs-ecs-worker/worker/queue-url \
  --query 'Parameter.Value' \
  --output text)

date && seq 1 10 | xargs -P 5 -I {} aws sqs send-message --queue-url $QUEUE_URL --message-body "{\"type\": \"test\", \"id\": \"msg_{}\"}" && date

		
出力
			
			Sun Oct 12 12:31:33 JST 2025
{
    "MD5OfMessageBody": "d0b8f7dac89e9cfcd8334aa4a8e0cf0a",
    "MessageId": "e612d5de-a324-4d45-a4cd-ac57a1a30b97"
}
{
    "MD5OfMessageBody": "7f920d8f057d1f229a8591c2d6e77cb2",
    "MessageId": "3200b809-bda9-4378-942c-5c6826dd82b7"
}
{
    "MD5OfMessageBody": "cf69a0029a10ccd213b242fc02b7d7f0",
    "MessageId": "cc61fa49-6762-4916-828f-2a7397d2242f"
}
{
    "MD5OfMessageBody": "bc3ef5071d583c2fdbea27fd8d095fca",
    "MessageId": "4781f1ad-ea7d-4e83-b7fe-196bc9dc0bf9"
}
{
    "MD5OfMessageBody": "e7de5c07eac255829ef765dd1b27c05c",
    "MessageId": "d0510014-a6e2-4ebc-812c-4b7ac3571e9f"
}
{
    "MD5OfMessageBody": "8d29f6ed7b9c2188a6baae6b3503e22a",
    "MessageId": "750ae696-01b5-47c6-bf3c-b39c91a225ae"
}
{
    "MD5OfMessageBody": "78fdec5c22093d570fc9219902313ba4",
    "MessageId": "3e231ea8-0e3a-407b-943f-b21cbb8c8b4a"
}
{
    "MD5OfMessageBody": "f2e60f1e1a9804fb94186e7f9dfe9410",
    "MessageId": "5f6c1fd5-69d5-42c6-9bf8-a1d184225d28"
}
{
    "MD5OfMessageBody": "358c45f2792a37e891d8826b31d8c55e",
    "MessageId": "4a8f3768-dc9c-4f75-8e52-33470f973119"
}
{
    "MD5OfMessageBody": "89652ac9606b69cede294b02b139c58c",
    "MessageId": "b97c48f2-63fd-4cc8-ad10-134f25a9a521"
}
Sun Oct 12 12:31:34 JST 2025

		

スケールされたことを確認して、環境変数を追加してデプロイします。

			
			ecspresso diff
date && ecspresso deploy && date

		

実行すると、3つはタスク保護が有効なため、一時的に6個のタスクが起動します。ログにも2025-10-12T12:35:59.847+09:00 (service sqs-ecs-worker) was unable to reach steady state because (taskSet ecs-svc/1111803033791694822) was unable to scale in due to (reason 3 tasks under protection)という記述が見られます。

CleanShot 2025-10-12 at 11.41.09@2x

出力
			
			2025-10-12T12:35:28.603+09:00 [INFO] ecspresso version: v2.6.0
--- arn:aws:ecs:ap-northeast-1:111111111111:service/sqs-ecs-worker/sqs-ecs-worker
+++ ecs-service-def.jsonnet
@@ -18,7 +18,6 @@
   "enableECSManagedTags": false,
   "enableExecuteCommand": true,
   "forceNewDeployment": false,
-  "healthCheckGracePeriodSeconds": 0,
   "networkConfiguration": {
     "awsvpcConfiguration": {
       "assignPublicIp": "ENABLED",

Sun Oct 12 12:35:29 JST 2025
2025-10-12T12:35:29.294+09:00 [INFO] ecspresso version: v2.6.0
2025-10-12T12:35:29.295+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] Starting deploy
Service: sqs-ecs-worker
Cluster: sqs-ecs-worker
TaskDefinition: sqs-ecs-worker:14
Deployments:
   PRIMARY sqs-ecs-worker:14 desired:3 pending:0 running:3 COMPLETED(ECS deployment ecs-svc/1111803033791694822 completed.)
AutoScaling:
  Capacity min:0 max:3
  Suspended in:false out:false scheduled:false
  Policy name:sqs-ecs-worker-sqs-ecs-worker-scaling-policy type:StepScaling
Events:
2025-10-12T12:35:29.986+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] Registering a new task definition...
2025-10-12T12:35:30.055+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] Task definition is registered sqs-ecs-worker:15
2025-10-12T12:35:30.164+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] deployment by ECS rolling update
2025-10-12T12:35:30.164+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] Updating service attributes...
2025-10-12T12:35:34.107+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] desired count: 3
2025-10-12T12:35:34.107+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] Updating service tasks...
2025-10-12T12:35:37.435+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] Waiting for service deployed...(it will take a few minutes)
2025-10-12T12:35:40.583+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] Waiting for service deployment hXiOsFOZada4d2XXZSxVV to complete...
2025-10-12T12:35:50.767+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker]  PRIMARY sqs-ecs-worker:15 desired:1 pending:1 running:0 IN_PROGRESS(ECS deployment ecs-svc/8835286666376237535 in progress.)
2025-10-12T12:35:50.767+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker]   ACTIVE sqs-ecs-worker:14 desired:2 pending:0 running:3 COMPLETED(ECS deployment ecs-svc/1111803033791694822 completed.)
2025-10-12T12:35:50.838+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker] Service deployment status: IN_PROGRESS
2025-10-12T12:35:50.663+09:00 (service sqs-ecs-worker) has started 1 tasks: (task 7f872694e9c9463089e62d6f855578af).
2025-10-12T12:35:59.847+09:00 (service sqs-ecs-worker) was unable to reach steady state because (taskSet ecs-svc/1111803033791694822) was unable to scale in due to (reason 3 tasks under protection)
2025-10-12T12:36:19.685+09:00 (service sqs-ecs-worker) has started 2 tasks: (task 457329dbd51347719d33451e3023d385) (task f3e0361f785d43fa918f5abe978c185a).
2025-10-12T12:36:20.741+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker]  PRIMARY sqs-ecs-worker:15 desired:3 pending:2 running:1 IN_PROGRESS(ECS deployment ecs-svc/8835286666376237535 in progress.)
2025-10-12T12:36:20.741+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker]   ACTIVE sqs-ecs-worker:14 desired:0 pending:0 running:3 COMPLETED(ECS deployment ecs-svc/1111803033791694822 completed.)
2025-10-12T12:36:20.760+09:00 (service sqs-ecs-worker) was unable to reach steady state because (taskSet ecs-svc/1111803033791694822) was unable to scale in due to (reason 3 tasks under protection)
2025-10-12T12:36:40.731+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker]  PRIMARY sqs-ecs-worker:15 desired:3 pending:0 running:3 IN_PROGRESS(ECS deployment ecs-svc/8835286666376237535 in progress.)
2025-10-12T12:36:40.731+09:00 [INFO] [sqs-ecs-worker/sqs-ecs-worker]   ACTIVE sqs-ecs-worker:14 desired:0 pending:0 running:3 COMPLETED(ECS deployment ecs-svc/1111803033791694822 completed.)

(中略)

		

ここからはECS Serviceのイベントを確認します。

CleanShot 2025-10-12 at 12.59.53@2x

  • 12:34 (UTC+9:00): service sqs-ecs-worker has started 3 tasks: task 96dac6ec0458426f8a6ce8fd53774fc7 task a80421f32af44fe583dde79a9e67fccd task e0c4cae8b1744c5c88eeed55a567e734.
    • SQSにメッセージを10件入れて3タスクにスケールアウト
  • 2025年10月12日 12:45 (UTC+9:00): service sqs-ecs-worker has stopped 3 running tasks: task a80421f32af44fe583dde79a9e67fccd task 96dac6ec0458426f8a6ce8fd53774fc7 task e0c4cae8b1744c5c88eeed55a567e734.
    • メッセージが処理し終わり、すぐスケールインが入っていることが確認できる
  • 2025年10月12日 12:45 (UTC+9:00): メッセージ: Successfully set desired count to 1. Change successfully fulfilled by ecs. 原因: monitor alarm sqs-ecs-worker-sqs-ecs-worker-scaling-alarm in state ALARM triggered policy sqs-ecs-worker-sqs-ecs-worker-scaling-policy
    • 同時に新旧3つずつの合計6タスク起動したので、キューのメッセージ数が4件になったのでスケールインが走る
      • 新しいバージョンのタスク3件がメッセージ処理し、停止
        • 7f872694e9c9463089e62d6f855578af
        • 457329dbd51347719d33451e3023d385(こちらは前項でもあったecs/svcのスケールイン対象外だが正常終了(exit 0)したため、Essential container in task exitedとして終了)
        • f3e0361f785d43fa918f5abe978c185a
  • 2025年10月12日 12:47 (UTC+9:00): service sqs-ecs-worker has started 1 tasks: task fb233df1e5584f47bfb20d44465f8d4d
    • 処理後すぐ1タスク起動し、残りのメッセージを処理

結果として、ローリングアップデート中でも取得したメッセージを最後まで処理してから停止することが確認できました。Auto Scalingとローリングアップデートの両方変化を追わないといけないので、少しわかりづらい点申し訳ありません。

さいごに

常駐プロセスの場合はAutoScalingやローリングアップデート時のスケールインで、タスク保護をしておけばメッセージの処理が終わるまで保護することが可能です。ただ保護解除後にすぐSIGTERMが発行されるわけではないので、ポーリングで次のメッセージを取得してしまうと、可視性タイムアウトを短くしたりタイムアウトまで待つ必要があります。なのでタスク保護を試みてfailuresがあれば正常終了(exit 0)する実装を試したという内容でした。

場合によってはEssential container in task exitedが発生してしまうので改善の余地がありますが、正常終了(exit 0)なのでどこまで気にするかにもよります。スケールインを考慮したGraceful shutdownの処理は最低限できたかなと思います。

通してまとまりがなくて申し訳ありません🙇‍♂️ より良い方法があればご教示いただけると幸いです。

脚注
  1. EventBridge Pipes等を使ってECSタスクを実行するパターンもあります。今回のパターンは、privateサブネットで実行する場合のECRのネットワーク転送量や常駐させれば処理までリードタイムの短さ、同時実行制御のしやすさ[2]があります。 ↩︎

  2. EventBridge Pipes → ECSタスク構成では ECS RunTask APIが直接呼び出されるため、ECSサービスではなく個別のECSタスクとして起動します。サービスのECSタスク数制限は使えません。その他参考より、EventBridgeとECSタスクは起動に成功した時点でPipesがSQSからメッセージを削除するという挙動も考慮が必要です。 ↩︎

この記事をシェアする

FacebookHatena blogX

関連記事

SQSをポーリングするECS構成でタスクスケールイン保護を試してみた | DevelopersIO