【Unit4 ブログリレー12日目】 エムスリーエンジニアリンググループの松山です。Unit4(m3.com開発チーム)のチームSREとしてなんでもやっています。
最近は手芸が好きで、紫外線硬化レジンで飼い猫や好きなキャラクターのグッズを作ったり、羊毛フェルトにひたすら針をさしたりしています。
この記事では、起動時の挙動が不安定なサービスのデプロイを安定させるため、ECS Blue/Greenデプロイへの切り替えを試してみた、という内容を書きます。

背景
とあるScalaで書かれているサービスを改修するため調べていたところ、起動直後の不安定さが気になりました。ECSではローリングアップデートによってデプロイを実施しているのですが、起動直後数分間のCPU利用率が高くなりがちでした。(実装が軽量な)ヘルスチェックは通るためタスクがHealthyと判定され、実際のリクエストが届いたところでAPIがGateway Timeoutを発生させるようになる、という事象がたまに起きるようになりました。
起動直後の挙動を改善するべくチーム内で調査を進めることになったのですが、それとは並行して別のアプローチについても検討を始めました。
AWS ECS Blue/Green デプロイ
2025年7月22日に AWS ECS Blue/Greenデプロイが発表されました。
ECS Blue/Greenデプロイ(以下BGデプロイと略します) は、ECSサービス内で完結してBGデプロイを行えるようにする仕組みです。これまでECS環境で同等の内容を実施するにはCode Deployの設定を書く必要がありましたが、これにより設定が簡略化されるとのことです。
そしてこのBGデプロイ中にhookを書けるとのことで、「BGデプロイ中にヘルスチェックが通ってから挙動が安定するまでの間1-2分待機時間を設ければいいのでは?」 と思いつきました。
以下は検証レベルではあるのですが、実装にあたってハマりポイントがいくつかあったため備忘録として共有したい、というのがこの記事の趣旨となります。
必要なリソース
- Blue相当となるalternative ターゲットグループ
- ECSがトラフィックを切り替えるためのリスナールール
- deploy hookとなるlambda関数
- ECSが ターゲットグループやリスナーの設定を変更するためのポリシー
- ECSが lambda関数をinvokeするためのロール
- BGデプロイのためのサービス設定変更
1,2,3 はイメージしやすいと思います。ECS BGデプロイではECSがそれぞれBlue/Green となる2つのターゲットグループ交互に使用することでBGデプロイを行います。
トラフィックの切り替えはECSが、BGデプロイ用に設定されているリスナールールをデプロイの段階によって切り替えたり合成したりして振り分けることにより行います。オプションとして、先行してGreen側に振り分けるトラフィックをリスナールールとして与えることも可能です(テストトラフィック)。
deploy hookは後述しますがBGデプロイの指定した段階で実行され、次の段階に進んでいいかどうかを返すLambda関数を用意しておくことで、こちらからデプロイの進行を制御できる、というものです。
問題は4,5 で、上記の挙動をすべてECSが主役となって行うため、ECSに対して各リソースへの権限を設定する必要があります。
実装
なお、チームにおけるterraformの大まかな実装方針についてはこちらの記事をお読みください。
ECSのヘルスチェック猶予時間は次のように設定しています。
health_check_grace_period_seconds = 300
(もちろんこの値も調整しているのですが、あんまり長くしてもデプロイにかかる時間が長くなります)
ターゲットグループとリスナールール
既存のターゲットグループに追加して、Green用のターゲットグループを作成します。
また、リスナールールを既存のリスナーの中に作成します。 今回はすべてのトラフィックを一度にGreenに向けてもらって構わないので、testトラフィックとproductionトラフィック用のリスナールールの内容は同じにしてあります。
Deploy hook
hook関数の説明の前にライフサイクルステージに軽く触れておきます。 いくつかあるステージのうち、lifecycle hookを実行させられるステージは次のようなものがあります。
| *ステージ名 | *説明 |
|---|---|
| PRE_SCALE_UP | Green側のターゲットグループにタスクを生やす前の状態 |
| POST_SCALE_UP | Green側のターゲットグループに必要数Healthyなタスクが生えた状態。今回はこのステージにdeploy hookを使います |
| TEST_TRAFFIC_SHIFT | testトラフィックをGreenに流す状態。この段階でGreen側のエラーが一定以上発生したらロールバックさせるようにしても良さそうです |
| POST_TEST_TRAFFIC_SHIFT | すべてのtestトラフィックがGreenに振り分けられた状態 |
| PRODUCTION_TRAFFIC_SHIFT | productionトラフィックをGreenに振り分けていく状態 |
| POST_PRODUCTION_TRAFFIC_SHIFT | すべてのproductionトラフィックがGreenに振り分けられた状態 |
この各ステージ中にlambda関数を実行させることができる、というのがdeploy hookです。
具体的には、"hookStatus": "SUCCEEDED" を返却すると次の段階へ進み、"hookStatus": "FAILED" を返却するとデプロイが失敗したとみなされてロールバックを開始します。今回は「一定時間待ったら次の段階に進んでください」というhookなので次のように書いてみます。
import json import logging import time logger = logging.getLogger() logger.setLevel(logging.INFO) def lambda_handler(event, context): logger.info(f"Event: {json.dumps(event)}") logger.info(f"Context: {context}") time.sleep(180) return { "hookStatus": "SUCCEEDED", }
権限設定
まず、ECSがターゲットグループやリスナールールを操作するためのroleを用意しましょう。
data "aws_iam_policy_document" "alb_service_role_assume_policy" { statement { effect = "Allow" actions = [ "sts:AssumeRole" ] principals { type = "Service" identifiers = [ "ecs.amazonaws.com" ] } } } resource "aws_iam_role" "alb_service_role" { name = "${var.application_env_name}-ecs-alb-service-role" assume_role_policy = data.aws_iam_policy_document.alb_service_role_assume_policy.json }
このroleにアタッチするポリシーなのですが、直近(8月10日時点)ではAmazonECSInfrastructureRolePolicyForLoadBalancers を渡せばいいことになりました。
# BGデプロイ用にECSに渡すIAMロール用ポリシー # https://docs.aws.amazon.com/ja_jp/aws-managed-policy/latest/reference/AmazonECSInfrastructureRolePolicyForLoadBalancers.html resource "aws_iam_role_policy_attachment" "alb_service_role_policy" { role = aws_iam_role.alb_service_role.name policy_arn = "arn:aws:iam::aws:policy/AmazonECSInfrastructureRolePolicyForLoadBalancers" }
次にECSが deploy hook Lambda関数をinvokeするためのroleを作りましょう。こちらは何を作ればいいかわかっていれば特に困ることは無いと思います。
# ECSのデプロイフック用のLambda関数のIAMロールとポリシー # https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/blue-green-permissions.html data "aws_iam_policy_document" "ecs_deploy_hook_policy" { statement { effect = "Allow" actions = [ "lambda:InvokeFunction", ] resources = [aws_lambda_function.deploy_hook.arn] } } resource "aws_iam_policy" "ecs_deploy_hook_policy" { name = "${var.application_env_name}-ecs-deploy-hook-policy" policy = data.aws_iam_policy_document.ecs_deploy_hook_policy.json } data "aws_iam_policy_document" "ecs_deploy_hook_role_assume_policy" { statement { actions = [ "sts:AssumeRole" ] principals { type = "Service" identifiers = [ "ecs.amazonaws.com" ] } } } resource "aws_iam_role" "ecs_deploy_hook_role" { name = "${var.application_env_name}-ecs-deploy-hook-role" assume_role_policy = data.aws_iam_policy_document.ecs_deploy_hook_role_assume_policy.json } resource "aws_iam_role_policy_attachment" "ecs_task_role_deploy_hook" { role = aws_iam_role.ecs_deploy_hook_role.name policy_arn = aws_iam_policy.ecs_deploy_hook_policy.arn }
Service設定の変更
ここまでできたら、あとはServiceリソースの設定を変更するだけです。
resource "aws_ecs_service" "main" { ... # BG deploy deployment_configuration { strategy = "BLUE_GREEN" bake_time_in_minutes = "3" lifecycle_hook { hook_target_arn = aws_lambda_function.deploy_hook.arn role_arn = aws_iam_role.ecs_deploy_hook_role.arn lifecycle_stages = ["POST_SCALE_UP"] } } # Load balancer load_balancer { ... # BGデプロイ用の設定 advanced_configuration { alternate_target_group_arn = var.alb_alternate_target_group_arn production_listener_rule = var.alb_listener_rule_blue_arn role_arn = aws_iam_role.alb_service_role.arn test_listener_rule = var.alb_listener_rule_green_arn } } ... }
lifecycle_hookのrole_arn にはECSが deploy hookをinvokeできるようにするためのロールを、advanced_configurationのrole_arn には ECSがターゲットグループやリスナールールを操作するためのロールをそれぞれ設定しましょう。
bake_time_in_minutes は、Greenへのトラフィック全面移行が終わった後、Blueに所属するタスクを生かしておく期間を設定します。この間であればGreen側でなにか問題を見つけたときにロールバックできます。
挙動を確認してみよう
では検証環境で挙動を確認してみましょう。先ほど修正したterraform をapplyし、コンソールを確認してみます。

デプロイ戦略がBlue/Greenデプロイに変わっていることがわかります。 なお、トラフィックの移行方法が「全て一度に」となっていますが、ここはまだ変更できません。ドキュメントを読む限りだと、今後すこしずつ割り振っていくようなオプションが追加されるのかと思われます。
また、ネットワーク設定を確認すると、次のようにBGデプロイ用の項目が追加されていることがわかります。

では、検証環境でlocustを用いて一定のリクエストを投げながら、サービスのデプロイを行い経過をみてみましょう。
aws ecs update-service --cluster <クラスタ名> --service <サービス名> --force-new-deployment

lambdaのログの抜粋
[INFO] ... Event:
{
...
"lifecycleStage": "POST_SCALE_UP",
...
}
...
REPORT RequestId: xxxxx Duration: 180003.61 ms Billed Duration: 180004 ms Memory Size: 128 MB Max Memory Used: 35 MB Init Duration: 123.55 ms
このlambdaが完了後、テストトラフィック, 本番トラフィックがGreen側に移行され、Blue側へのトラフィックはなくなります。
そしてベークタイム中になると、すべてのトラフィックはGreen側に流れていますが、Blue側のタスクもまだスタンバイしています。
ベークタイムが経過すると最終的にBlue側のタスクは消えます。
次のcloudwatchのグラフにそれぞれのターゲットグループにきたリクエスト数の状況を示します。

ここで緑矢印の時間帯がスケールアップ中、赤い矢印がdeploy hookによってデプロイプロセスが止まっている段階となります。 そしてトラフィック数が逆転し、青矢印のベークタイムの経過中もとのターゲットグループへのリクエストは0 (=まだターゲットグループやタスクは生きている) ということがわかります。
まとめ
今回はAWS ECS Blue/Greenデプロイを既存のローリングアップデートを採用しているプロダクトに導入するには、という内容を書きました。発表直後と比べると多少公式ドキュメントが増えてきたような気はするのですが、当初はほぼ発表のブログだけだったので情報が足りず、権限周りを試行錯誤した記憶があります。
Unit4のチームSREでは、チームが持つ全てのサービスの運用を開発メンバーやビジネスチームと連携しながら改善を続けています。
We are Hiring!
エムスリーでは一緒にサービスの信頼性を高めていけるエンジニアを絶賛募集中です。ご興味ある方は是非カジュアル面談等ご応募ください!