はじめに

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デプロイのresourceArn
  • status: デプロイ状態 (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で公開しています。