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

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

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

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

※デプロイパイプラインのサンプルは[以前の記事](https://qiita.com/neruneruo/items/d5e58b4013debdf9ebbe)で書いているので参考までに。

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

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

## Appspecの編集
Appspecでは、CodeDeployの各ライフサイクルイベントをフックしてLambda関数をコールすることができるため、そのフックの設定(Hooks)を設定する。
今回は、ECSタスクのデプロイ後、ELBのテストポート側のヘルスチェックが完了した際に呼ばれるライフサイクルイベントについてフックする。
詳細は[公式のドキュメント](https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/reference-appspec-file-structure-hooks.html)を参照。

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

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

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

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

```python
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を見ない場合、ロールバックされなかったらうまくいった、と考えるしかない)。つまり、明示的に成功したことが分かるログにしておかないと、判断に迷うことになる。しかし、わざわざログを別画面に見に行ってからボタンを押すという複雑な手順にするのは、なんともクラウドネイティブではなくて格好悪い……

![キャプチャ2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/8df73541-dfe2-4ed9-8484-66f14b709380.png)

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

![キャプチャ3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/ee577841-299f-29b9-69e2-98c6254dda14.png)


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

![キャプチャ1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/c3605de5-7081-83e1-2ba4-d24cbc49b390.png)

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