CloudFormationでEC2インスタンスの起動が完了したらスタックの更新を続行する方法
EC2インスタンスの起動が完了したらスタックの更新を続行する方法
結論
AWS::CloudFormation::Initを使いましょう!
ということなのですが、ここで出てくるcfn-init
やらcfn-signal
にかなりハマったので、調査した結果と実際の使用方法を紹介します。
きっかけ
とあるプロジェクトで、
「EC2がALBのヘルスチェックでhealthy
になるまで更新を一時停止したい」
という目的がありました。
なぜ?
CloudFormationによるAuto Scalingグループの置換更新で起動したEC2インスタンスにアクセスできなかったためです。
問題は、
「ALBのヘルスチェックの結果に関わらず、CloudFormationの更新が完了する」
ということでした。
具体的には、新しく起動したEC2インスタンスに対するALBのヘルスチェックの結果がunhealthy
なのに、古いAuto ScalingグループとEC2インスタンスがCloudFormationにより終了してしまうという挙動です。Auto Scalingグループ内のEC2インスタンスの数は常時1台という要件なので、複数台を1台ずつ更新するというローリングアップデートも使用できませんでした。
イメージ
ざっくりしたイメージですが、やっていることはこんなかんじです。
問題のパターン
- 更新リクエストを受けたCloudFormationは新しいAuto Scalingグループを起動し、Auto ScalingグループがEC2起動、ALBに登録を実施
- 新しいEC2が起動したら、ALBのヘルスチェックの結果にかかわらず、CloudFormationは古いAuto Scalingグループ削除を開始
- 古いグループの削除が完了したら、CloudFormationは更新完了通知をSNSからChatbot経由でSlackに送信
3の時点で新しいEC2がALBのヘルスチェックをクリアしていないと、Developerはアクセスできません。
また、Auto Scalingグループのヘルスチェックの猶予期間を超えてもヘルスチェックをクリアしない場合、EC2は異常と判断され、新しいEC2が起動します。2台目がヘルスチェックを猶予期間内にクリアできればアクセスできるようなりますが、何度やってもクリアできない場合は終了→起動の無限ループが発生します。
上記より、
「ALBのヘルスチェックの結果に関わらず、CloudFormationの更新が完了する」
ことが問題だと判断し、対応を考えました。
解決パターン
上記の問題を解決するために、以下の挙動を目指しました。
- EC2がALBのヘルスチェックで
healthy
になったらCloudFormationによる更新を続行 - 一定時間内に
healthy
にならなかったらCloudFormationによる更新を中断してロールバックする
これにより、
- 更新完了通知が届いた時点で新しいEC2にアクセスできる
- 更新が失敗した場合でも古いEC2は保持されているので、アクセスが失われることはなくなる
となり、新旧どちらにもアクセスできなくなるという事態は防げます。
しかし、1つだけ課題があります。
「一時的に2台のインスタンスにアクセスできる時間が発生する」
ということです。
ただし、これは後述するメンテナンス画面により解決しました。
解決手順
- CloudFormationテンプレートファイルの修正
- カスタムAMIからユーザーデータを実行する設定
- ALBリスナールールの追加と自動切換え
CloudFormationテンプレートファイルの修正
以下の修正を行いました。
- Auto Scalingグループの更新方法の修正
- 起動テンプレートにユーザーデータを追記
- Auto Scalingグループに
AWS::CloudFormation::Init
を追記
1. Auto Scalingグループの更新方法の修正
もともとAuto Scalingグループの更新は以下のような設定でした。
"UpdatePolicy": {
"AutoScalingReplacingUpdate": {
"WillReplace": "true"
}
},
"CreationPolicy": {
"ResourceSignal": {
"Count": 0
}
}
変更後は以下のようになりました。
"UpdatePolicy": {
"AutoScalingReplacingUpdate": {
"WillReplace": "true"
}
},
"CreationPolicy": {
"ResourceSignal": {
"Count": 1, //変更
"Timeout": "PT15M" //追記
}
}
この変更により、起動したEC2から成功シグナルが15分以内に送信されなければロールバックするという挙動になります。
詳しくはUpdatePolicy 属性 - AWS CloudFormation、CreationPolicy 属性 - AWS CloudFormationをご覧ください。
2. 起動テンプレートにユーザーデータを追記
こちらを参考に"Type": "AWS::EC2::LaunchTemplate"
に以下を追記しました。
"UserData": {
"Fn::Base64": {
"Fn::Join": [
"",
[
"<script>\n",
"cfn-init.exe -v -s ",
{
"Ref": "AWS::StackId"
},
" -r <Auto Scalingグループの論理ID>",
" --configsets <cfn-initで実行するconfigSets名>",
" --region ",
{
"Ref": "AWS::Region"
},
"\n",
"cfn-signal.exe -e %ERRORLEVEL% --stack ",
{
"Ref": "AWS::StackId"
},
" --resource <Auto Scalingグループの論理ID> --region ",
{
"Ref": "AWS::Region"
},
" --success true",
"\n",
"</script>",
"<persist>true</persist>"
]
]
}
},
上記により以下のような挙動になります。
-
UserData
がcfn-init.exe
を呼び出す -
configsets
で後述のAWS::CloudFormation::Init
内の処理を実行する - 処理が完了したら
cfn-signal.exe
で成功シグナルを送信する - CloudFormationは成功シグナルを受け取ったら、Auto Scalingグループは更新完了と判断して後続の更新を行う。
AWS::CloudFormation::Init
を追記
3. Auto Scalingグループにこちらを参考に、"Type": "AWS::AutoScaling::AutoScalingGroup"
に以下を追記しました。
"Metadata": {
"AWS::CloudFormation::Init": {
"configSets": {
"HealthCheck": ["verify_instance_health"]
},
"verify_instance_health": {
"files": {
"C:\\cfn\\healthcheck.ps1": {
"content": {
"Fn::Join": [
"",
[
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
"\n",
"$data = Invoke-WebRequest http://169.254.169.254/latest/meta-data/instance-id -UseBasicParsing",
"\n",
"$id = $data.Content",
"\n",
"$state = Get-ELB2TargetHealth -<ターゲットグループのARN> -Target @{Id=$id} -Select TargetHealthDescriptions.TargetHealth.State",
"\n",
"echo $state >> C:\\cfn\\state.log",
"\n",
"while ($state -ne \"healthy\") {",
"$state = Get-ELB2TargetHealth -<ターゲットグループのARN> -Target @{Id=$id} -Select TargetHealthDescriptions.TargetHealth.State",
"\n",
"echo $state >> C:\\cfn\\state.log",
"\n",
"Start-Sleep -s 10",
"\n",
"}",
"\n",
"echo \"done\" >> C:\\cfn\\state.log"
]
]
}
}
},
"commands": {
"ELBHealthCheck": {
"command": "powershell.exe -ExecutionPolicy Bypass C:\\cfn\\healthcheck.ps1",
"waitAfterCompletion": 0
}
}
}
}
},
上記により以下のような挙動になります。
- 先述の
cfn-init.exe
により実行される -
C:\cfn\
にhealthcheck.ps1
というPowerShellファイルを作成 - ファイルには以下のスクリプトが記述される
- スクリプトについて詳しくはこちらをご覧ください。
# UTF8にエンコード
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# 起動したEC2インスタンス自身のIDを取得
$data = Invoke-WebRequest http://169.254.169.254/latest/meta-data/instance-id -UseBasicParsing
$id = $data.Content
# ALBによるヘルスチェックの結果を取得
$state = Get-ELB2TargetHealth -TargetGroupArn arn:aws:elasticloadbalancing:ap-northeast-1:467964259659:targetgroup/Study-alb-tg/dded41d146814194 -Target @{Id = $id} -Select TargetHealthDescriptions.TargetHealth.State
echo $state >> C:\cfn\state.log
# ヘルスチェックをクリアするまで10秒ごとに結果を取得
while ($state -ne "healthy") {
$state = Get-ELB2TargetHealth -TargetGroupArn arn:aws:elasticloadbalancing:ap-northeast-1:467964259659:targetgroup/Study-alb-tg/dded41d146814194 -Target @{Id = $id } -Select TargetHealthDescriptions.TargetHealth.State
Start-Sleep -s 10
echo $state >> C:\cfn\state.log
}
echo "done" >> C:\cfn\state.log
-
healthcheck.ps1
が実行される
カスタムAMIからユーザーデータを実行する設定
詳しくはこちらをご覧ください。
ALBリスナールールの追加と自動切換え
詳しくはこちらにまとめましたが、以下の手順を実施しました。
- ALBのリスナールールにメンテナンスページを追加
- リスナールールを切り替えるLambda関数を作成
- CloudFormation → SNS → Lambda → ALBの経路で自動切換え
まとめ
今回はEC2インスタンスの起動が完了したらスタックの更新を続行する方法を紹介しました。
cfn-init
、cfn-signal
だけでなくPowerShellスクリプトの作成やカスタムAMIからユーザーデータを実行する方法など、ハマりまくりました。改めて以下にポイントをまとめておきます。
-
CreationPolicy
でCount
とTimeout
を指定する - ユーザーデータから
cfn-init.exe
を呼び出す -
cfn-init.exe
からAWS::CloudFormation::Init
の処理を実行する -
AWS::CloudFormation::Init
の処理が終了したらcfn-signal
を送信する - スクリプトは頑張って作る
- カスタムAMIからユーザーデータを実行できるように設定する
- ALBのリスナールールでメンテナンス画面を表示する
今後何かの参考になれば幸いです。
参考資料
AWS::CloudFormation::Init - AWS CloudFormation
Auto Scaling インスタンスのヘルスチェック - Amazon EC2 Auto Scaling (日本語)
UpdatePolicy 属性 - AWS CloudFormation
CreationPolicy 属性 - AWS CloudFormation
AWS CloudFormation の Amazon EC2 Windows インスタンスから送信される CREATE_COMPLETE シグナルのトラブルシューティング
aws-cloudformation-templates/AutoScalingRollingUpdates.yaml at master · awslabs/aws-cloudformation-templates · GitHub
Discussion