概要

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

構成図

実装

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の環境変数に書いています。

# -*- 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 AmazonEC2ReadOnlyAccessautoscaling:CompleteLifecycleAction あたりを行えるよう許可します。

StepFunctions

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

{
  "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のライフサイクルアクションからステートマシンを呼び出すよう設定します。

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

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

実行結果

スケールアウトが発生するとスケールアウトした数だけStepFunctionが動いてコンテンツ同期状況をチェックしてくれます。

参考

元記事はこちら

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