ECSのデプロイ中の正常性確認を自動化するAppspecのHooks設定


はじめに

デプロイに失敗した際に自動でロールバックできるというのは重要だ。
ECSのCodeDeployによるデプロイでは、End-to-Endの正常性確認を自動化することができる。
今回はチュートリアルとほぼ同じ内容ではあるが、AppspecのHooks設定による正常性確認のやり方を整理する。

前提条件

少なくとも以下のことを理解していた方が飲み込みが早いはずである。

  • ECS+Fargateのデプロイパイプラインを自分で作ったことがある

※デプロイパイプラインのサンプルは以前の記事で書いているので参考までに。

やること

パイプラインの整理までできているのであれば、それほど大変ではない。
以下の作業をやればよい。

  • Appspecの編集
  • IAMポリシーの点検
  • 正常性確認用Lambda関数の作成

Appspecの編集

Appspecでは、CodeDeployの各ライフサイクルイベントをフックしてLambda関数をコールすることができるため、そのフックの設定(Hooks)を設定する。
今回は、ECSタスクのデプロイ後、ELBのテストポート側のヘルスチェックが完了した際に呼ばれるライフサイクルイベントについてフックする。
詳細は公式のドキュメントを参照。

Hooks:
  - AfterInstall: "[後で設定するLambda関数名]"

これだけで良い。Arnで指定しているケースもあるが、関数名でも実行することができた。

IAMポリシーの点検

CodeDeployのサービスロールに lambda:InvokeFunction が入っていることを確認する。
Hooksの仕組みは、CodeDeployからLambda関数を実行するため、このパーミッションが無いとエラーになる。

正常性確認用Lambda関数の作成

キモになるのがこのLambdaである。
boto3でCODE_DEPLOYのクライアントを作り、アサーションの結果をput_lifecycle_event_hook_execution_statusで通知する。ステータスで Failed を渡すと、自動でデプロイがロールバックされる。気を付けなければいけないのは Succeeded を渡した場合は「自動で進む」ではなく「エラーにならないだけ」 なので、操作者が自分で次のフェーズに進めてあげないといけない。

import json
import urllib.request
import boto3

CODE_DEPLOY = boto3.client('codedeploy')

def notify_execution_status(event, status):
    deployment_id = event.get('DeploymentId')
    execution_id = event.get('LifecycleEventHookExecutionId')

    return CODE_DEPLOY.put_lifecycle_event_hook_execution_status(deploymentId=deployment_id,
                                                                 lifecycleEventHookExecutionId=execution_id,
                                                                 status=status)

def lambda_handler(event, context):
    print(event)

    status = 'Succeeded'

    try:
        request = urllib.request.Request("http://[ELBのドメイン]:[テストポート]/[リソース]", method="GET")
        with urllib.request.urlopen(request) as response:
            response_body = response.read().decode("utf-8")
        assert [任意のアサーション]
        notify_response = notify_execution_status(event, status)
    except Exception as e:
        print(e)
        status = 'Failed'
        notify_execution_status(event, status)
    else:
        print(notify_response)    

さて、この中で実行している CODE_DEPLOY.put_lifecycle_event_hook_execution_status は、CodeDeployの状態を変えるものであるため、このLambda関数にそのパーミッションを付与する必要がある。

Lambdaの実行IAMロールのActionsに codedeploy:PutLifecycleEventHookExecutionStatus を入れておこう。

実行結果

上記の設定の入ったCodeDeployを動かすと、以下のようにCloudWatch LogsにLambda関数の結果が出力される。
難点は、CodeDeploy側からこの関数が上手くいったかの確認ができないということだ(CloudWatch Logsを見ない場合、ロールバックされなかったらうまくいった、と考えるしかない)。つまり、明示的に成功したことが分かるログにしておかないと、判断に迷うことになる。しかし、わざわざログを別画面に見に行ってからボタンを押すという複雑な手順にするのは、なんともクラウドネイティブではなくて格好悪い……

上記の確認後に、以下のCodeDeployコンソール画面の「トラフィックの再ルーティング」のボタンを押すことでデプロイを完了させられる。

ちなみに、Lambda関数がFailedを返した場合は以下のように出力される。

実はこれ、うまくシナリオをLambdaで組むことができれば、End-to-Endのインテグレーション試験なんかもCIに組み込んで自動化できるということだな……。工夫の余地がありそうだ。