AutoScalingのライフサイクルフックでStepFunctionsを呼び出して処理を行う

# 概要
AutoScalingするWebサーバーにCMSサーバーからコンテンツ同期を行っている環境があり、ELBに追加する前にコンテンツ同期が終わっているか確認したかったのでAutoScalingライフサイクルフックでStepFunctionsを呼び出して外部公開前にコンテンツ同期のチェックとライフサイクルアクションを完了させるようにしてみました。

# 構成図
![AWS Icons.png](https://qiita-image-store.s3.amazonaws.com/0/48250/e04437d2-e906-f28c-5733-6867b19a265c.png)


# 実装
## AutoScaling
コマンドもしくはコンソールからインスタンスの起動 `autoscaling:EC2_INSTANCE_LAUNCHING` なライフサイクルフックを追加します。
https://docs.aws.amazon.com/ja_jp/autoscaling/ec2/userguide/lifecycle-hooks.html#adding-lifecycle-hooks

## Lambda
コンテンツの同期チェックとライフサイクルフックを完了させるLambdaFunctionを作成します。外部にはELB経由で公開しているので、LambdaはVPC内で起動しインスタンスのプライベートIPに確認しに行きます。
確認するURLはスクリプトを使いまわせるようにLambdaの環境変数に書いています。

```python
# -*- coding: utf-8 -*-
import boto3
import hashlib
import os
import requests

ec2 = boto3.resource('ec2')
autoscaling = boto3.client('autoscaling')


def lambda_handler(event, context):
    """
    AutoScalingのCloudWatch Events[EC2 Instance-launch Lifecycle Action]から起動。
    コンテンツの同期を確認、完了していればLifecycle Actionを完了させELBに組み込む。
    """
    print(event)
    try:
        instance_id = event['detail']['EC2InstanceId']
        instance = ec2.Instance(instance_id)
        host = os.environ['Host']
        headers = {'Host': host}

        origin_req = {'timeout': 5}
        replica_req = {'timeout': 5, 'headers': headers}

        # TOPページをチェック
        origin_top = requests.get('https://{}/'.format(host), **origin_req)
        replica_top = requests.get('http://{}/'.format(instance.private_ip_address), **replica_req)

        # TOPページのコンテンツを比較
        top_result = check_content(origin_top, replica_top)

        if top_result is True:
            # Lifecycle Actionを完了
            print('Content sync is complete.')
            response = autoscaling.complete_lifecycle_action(
                LifecycleHookName=event['detail']['LifecycleHookName'],
                AutoScalingGroupName=event['detail']['AutoScalingGroupName'],
                LifecycleActionResult='CONTINUE',
                InstanceId=instance_id
            )
            print(response)
        else:
            class ContentUnsyncException(Exception):
                """
                StepFunctionに認識させるコンテンツが同期が完了していない例外
                """
                pass

            raise ContentUnsyncException('top_result:{} pre_result:{}'.format(top_result, pre_result))

    except Exception as e:
        print(e)
        raise e


def check_content(origin, replica):
    origin_hash = hashlib.sha256(origin.text.encode('utf-8')).hexdigest()
    replica_hash = hashlib.sha256(replica.text.encode('utf-8')).hexdigest()
    return origin_hash == replica_hash
```

IAM Roleは `AWSLambdaVPCAccessExecutionRole` `AmazonEC2ReadOnlyAccess` と`autoscaling:CompleteLifecycleAction` あたりを行えるよう許可します。

## StepFunctions

Lambdaを呼び出すステートマシンを作成します。
コンテンツ同期に少し時間がかかるのでリトライを設定しています。

```json
{
  "Comment": "Check the sync status of content at instance startup.",
  "StartAt": "CheckContent",
  "States": {
    "CheckContent": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:${region}:${account_id}:function:check_content",
      "Retry": [
        {
          "ErrorEquals": [
            "ContentUnsyncException",
            "States.TaskFailed",
            "States.Timeout"
          ],
          "IntervalSeconds": 30,
          "MaxAttempts": 10,
          "BackoffRate": 1.0
        }
      ],
      "End": true
    }
  }
}
```

IAM Roleは `lambda:InvokeFunction` を許可します。

## CloudWatchイベント

CloudWatchイベントでAutoScalingのライフサイクルアクションからステートマシンを呼び出すよう設定します。

```json
{
  "detail": {
    "AutoScalingGroupName": [
      "${autoscalinggroupname}"
    ]
  },
  "detail-type": [
    "EC2 Instance-launch Lifecycle Action"
  ],
  "source": [
    "aws.autoscaling"
  ]
}
```

IAM Roleは `states:StartExecution` を許可します。
StepFunctionsとCloudWatchはコンソールから作成するとRoleもいい感じのが作成されるかと思います。

# 実行結果
スケールアウトが発生するとスケールアウトした数だけStepFunctionが動いてコンテンツ同期状況をチェックしてくれます。
<img width="716" alt="Step_Functions_Management_Console.png" src="https://qiita-image-store.s3.amazonaws.com/0/48250/c4ebf12a-aa60-3930-139a-1df7e1beb987.png">

# 参考
- [Amazon EC2 Auto Scaling ライフサイクルフック](https://docs.aws.amazon.com/ja_jp/autoscaling/ec2/userguide/lifecycle-hooks.html)
- [Amazon VPC 内のリソースにアクセスできるように Lambda 関数を構成する](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/vpc.html)
- [CloudWatch イベント を使用してステートマシンの実行を開始する](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/tutorial-cloudwatch-events-target.html)
- [Automating AWS Lambda Function Error Handling with AWS Step Functions](https://aws.amazon.com/jp/blogs/compute/automating-aws-lambda-function-error-handling-with-aws-step-functions/)