ECS Deployment Lifecycle Hookを使った手動デプロイ制御の実装
公開日: 2025/07/21
はじめに
Amazon ECSでは、サービスのデプロイ時にLifecycle Hookを使用してデプロイプロセスを制御できます。 DynamoDBとLambda関数を組み合わせて、手動承認フローを持つデプロイを実装しました。
流れ
- ① ユーザーはデプロイを開始する
- ②③ ECSはLambdaをHookで起動しDynamoDBにデプロイIDをキーとするデータを作成する
- ④⑤⑥ ユーザーはテストリスナーで新しいタスクを確認し問題ないことをLambdaに通知する
- ⑦⑧⑨⑩ ECSはユーザーが確認完了を通知するまでLambdaを定期的に起動しつづけ完了していれば切り替えをして終了する
sequenceDiagram autonumber actor User participant ECS participant LambdaHook participant DynamoDB User ->> ECS : デプロイ ECS ->> LambdaHook: 初期化時フック LambdaHook ->> DynamoDB : データ追加(status:IN_PROGRESS) ECS ->> User : テストリスナー準備完了(Slack通知など) User ->> LambdaHook : 確認完了 LambdaHook ->> DynamoDB : データ更新(status:CONFIRMED) loop 確認完了まで ECS ->> LambdaHook: テスト状況確認 LambdaHook ->> DynamoDB : ステータス確認 DynamoDB -->> LambdaHook : status alt status=CONFIRMED ECS ->> ECS : 切り替え実施 end end
実装詳細
1. DynamoDB テーブル設計
デプロイ状態を管理するためのテーブル構成でデプロイ毎にデプロイIDをキーとするデータを作成する。
resource "aws_dynamodb_table" "hook_state" {
name = "ecs_service_hook_state"
billing_mode = "PAY_PER_REQUEST"
hash_key = "deploy_id"
attribute {
name = "deploy_id"
type = "S"
}
}
テーブルスキーマ:
deploy_id
: ECSデプロイのresourceArnstatus
: デプロイ状態 (IN_PROGRESS, CONFIRMED, FINISH)createdAt
: 作成日時updatedAt
: 更新日時
2. Lambda関数の実装
状態管理ロジック
テーブルは初期作成のIN_PROGRESS→手動承認済みのCONFIRMED→完了時のFINISHと遷移します。
class TableStatus(Enum):
IN_PROGRESS = "IN_PROGRESS" # デプロイ開始
CONFIRMED = "CONFIRMED" # 手動承認済み
FINISH = "FINISH" # デプロイ完了
UNKNOWN = "UNKNOWN" # 不明な状態
HookイベントがECSから送信されますが、手動承認用のHOOKとしてX_MANUAL_CONFIRMを追加しています。
class HookStatus(enum.Enum):
RECONCILE_SERVICE = "RECONCILE_SERVICE"
PRE_SCALE_UP = "PRE_SCALE_UP"
POST_SCALE_UP = "POST_SCALE_UP"
TEST_TRAFFIC_SHIFT = "TEST_TRAFFIC_SHIFT"
POST_TEST_TRAFFIC_SHIFT = "POST_TEST_TRAFFIC_SHIFT"
PRODUCTION_TRAFFIC_SHIFT = "PRODUCTION_TRAFFIC_SHIFT"
POST_PRODUCTION_TRAFFIC_SHIFT = "POST_PRODUCTION_TRAFFIC_SHIFT"
X_MANUAL_CONFIRM = "X_MANUAL_CONFIRM"
UNKNOWN = "UNKNOWN"
メイン処理フロー
メイン処理ではHOOKのステージに応じてシーケンス図にあるとおりにDynamoDBの更新をしていきます。
def handler(event, context):
hook_event = HookEvent(event)
lifecycle_stage = hook_event.lifecycle_stage
deploy_id = hook_event.resource_arn
# ルーティング
if lifecycle_stage == HookStatus.PRE_SCALE_UP:
return handle_pre_scale_up(deploy_id)
elif lifecycle_stage == HookStatus.X_MANUAL_CONFIRM:
return handle_manual_confirm(deploy_id)
elif lifecycle_stage == HookStatus.POST_TEST_TRAFFIC_SHIFT:
return handle_post_test_traffic_shift(deploy_id)
3. デプロイフロー
起動時はDynamoDBにデータを作成する。
ステップ1: PRE_SCALE_UP
def handle_pre_scale_up(deploy_id: str):
try:
createTableStatus(deploy_id, TableStatus.IN_PROGRESS)
return {"hookStatus": "SUCCEEDED"}
except Exception as e:
logger.error(f"Error: {e}")
return {"hookStatus": "FAILED"}
ステップ2: 手動承認 (X_MANUAL_CONFIRM)
手動承認時にDynamoDBを確認済みに変更する。
def handle_manual_confirm(deploy_id: str):
try:
current_status = getTableStatus(deploy_id)
if current_status == TableStatus.IN_PROGRESS:
updateTableStatus(deploy_id, TableStatus.CONFIRMED)
return {"hookStatus": "IN_PROGRESS"}
else:
return {"hookStatus": "FAILED"}
except Exception as e:
return {"hookStatus": "FAILED"}
ステップ3: POST_TEST_TRAFFIC_SHIFT
DynamoDBで確認済みであれば、DynamoDBをFINISHに更新した後に切り替え作業に進む(hookStatus=SUCCEEDED)。
def handle_post_test_traffic_shift(deploy_id: str):
try:
current_status = getTableStatus(deploy_id)
if current_status == TableStatus.CONFIRMED:
updateTableStatus(deploy_id, TableStatus.FINISH)
return {"hookStatus": "SUCCEEDED"}
elif current_status == TableStatus.IN_PROGRESS:
return {"hookStatus": "IN_PROGRESS"} # 承認待ち
else:
return {"hookStatus": "FAILED"}
except Exception as e:
return {"hookStatus": "FAILED"}
まとめ
本番環境で手動承認のフローを実装しました。作成したような仕組みで慎重なデプロイ運用が可能になります。 CodeDeployでは再ルーティングを手動でできましたが、ECSでも今後可能になるかもしれません。
サンプルコード
サンプルコードは GitHubで公開しています。