はじめに
CloudFormationで「Auto Scalingグループ」を作っていると、設定を変えたときに裏でサーバーがどう入れ替わるかまで気にすることは少ないかもしれません。今回、この入れ替え方法に新しい選択肢 AutoScalingInstanceRefresh が追加されたとのことなので、実際に手を動かして挙動を確かめてみました。
忙しい人のための要約
- CloudFormationのAuto Scalingグループを更新したとき、サーバーをどう入れ替えるかを決める設定(更新ポリシー)に、新しく
AutoScalingInstanceRefreshが追加された - 従来の
AutoScalingRollingUpdateは、入れ替え中はスケールインの保護などの機能を使えなかった。新しい方式ではそういった機能を止めずに更新できる - 更新に失敗したときの巻き戻し(ロールバック)は、CloudFormationのスタックの巻き戻しにまとめて任される形になった
AutoScalingInstanceRefreshポリシーとは
Auto Scalingグループには、既存のサーバーを新しい設定の内容で少しずつ入れ替える「インスタンスリフレッシュ」という機能が元々あります。今回のアップデートは、CloudFormationでスタックを更新したときに、この入れ替え機能をそのまま呼び出せるようにしたものです。
CloudFormationは、以下のどれかを変更したときに AutoScalingInstanceRefresh を実行します(それ以外の設定変更では動きません)。
-
LaunchTemplate(サーバーの起動設定。OSの種類やサイズなどをまとめたもの) -
MixedInstancesPolicy(複数の種類・購入方式のサーバーを混ぜる設定) -
VPCZoneIdentifier(サーバーを置くサブネット、いわばネットワークの区画) -
AvailabilityZones/AvailabilityZoneIds(サーバーを置くデータセンターの単位) -
PlacementGroup(サーバー同士の物理的な配置ルール)
これまで主流だった AutoScalingRollingUpdate との一番の違いは、入れ替え中もAuto Scalingグループ本来の機能を止めずに使える点です。従来の方式は、意図せず入れ替わってしまうサーバーを守る「スケールイン保護」に対応しておらず、干渉を避けるために SuspendProcesses でヘルスチェックなどをいったん止めるのが定石でした。新しい方式ではその必要がありません。
失敗したときの巻き戻し方も変わります。どちらの方式も「CloudFormationのスタックロールバック」で巻き戻す点は同じですが、その中身が違います。従来方式は、ロールバック時にも「まとまった数ずつ入れ替える」処理をCloudFormation自身がもう一度やり直す形でした。新しい方式では、ロールバック時にAuto Scaling本来のインスタンスリフレッシュ機能が新たに起動され、元の設定に戻していきます。Auto Scaling側にある入れ替えを取り消すAPI(RollbackInstanceRefresh)は、CloudFormationが始めた入れ替えには使えないので注意してください。
やってみた
UpdatePolicy属性のドキュメントを参考に、最小構成でスタックを作り、LaunchTemplate を書き換えたときに本当に入れ替えが起きるかを確認しました。以下はそのままコピペで再現できる手順です。デフォルトVPCとt3.microサーバー最大4台を数分間使うだけなので、費用はごくわずかです。
手順1: 必要な情報を集めて設定ファイルを作る
まず、お使いのAWSアカウントのデフォルトVPCの情報と、最新のAmazon Linux 2023のサーバーイメージIDを自動で取得します。
export AWS_REGION=ap-northeast-1
VPC_ID=$(aws ec2 describe-vpcs --region $AWS_REGION \
--filters "Name=isDefault,Values=true" --query 'Vpcs[0].VpcId' --output text)
SUBNET_IDS=$(aws ec2 describe-subnets --region $AWS_REGION \
--filters "Name=vpc-id,Values=$VPC_ID" \
--query 'Subnets[*].SubnetId' --output text | tr '\t' ',')
SG_ID=$(aws ec2 describe-security-groups --region $AWS_REGION \
--filters "Name=vpc-id,Values=$VPC_ID" "Name=group-name,Values=default" \
--query 'SecurityGroups[0].GroupId' --output text)
AMI_ID=$(aws ssm get-parameter --region $AWS_REGION \
--name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
--query 'Parameter.Value' --output text)
echo "VPC_ID=$VPC_ID / SUBNET_IDS=$SUBNET_IDS / SG_ID=$SG_ID / AMI_ID=$AMI_ID"
次に、CloudFormationのテンプレートをファイルに書き出します。t3.microのサーバーを2台(最大4台まで増やせる設定)用意し、UpdatePolicy に AutoScalingInstanceRefresh を指定しています。新しいサーバーを先に起動してから古いサーバーを止める動きを見たいので MinHealthyPercentage: 100 / MaxHealthyPercentage: 200にしました。
template.yamlの中身
cat > template.yaml <<'EOF'
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
AmiId:
Type: String
SubnetIds:
Type: CommaDelimitedList
SecurityGroupId:
Type: String
AppVersion:
Type: String
Default: v1
Resources:
LaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateData:
ImageId: !Ref AmiId
InstanceType: t3.micro
SecurityGroupIds:
- !Ref SecurityGroupId
UserData:
Fn::Base64: !Sub |
#!/bin/bash
echo "${AppVersion}" > /tmp/app_version.txt
ASG:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
AutoScalingGroupName: instance-refresh-verify-asg
VPCZoneIdentifier: !Ref SubnetIds
LaunchTemplate:
LaunchTemplateId: !Ref LaunchTemplate
Version: !GetAtt LaunchTemplate.LatestVersionNumber
MinSize: '1'
MaxSize: '4'
DesiredCapacity: '2'
HealthCheckType: EC2
HealthCheckGracePeriod: 60
UpdatePolicy:
AutoScalingInstanceRefresh:
Strategy: Rolling
Preferences:
MinHealthyPercentage: 100
MaxHealthyPercentage: 200
InstanceWarmup: 60
SkipMatching: true
EOF
パラメータ(テンプレートに渡す値)もファイルにしておきます。
cat > params.json <<EOF
[
{"ParameterKey": "AmiId", "ParameterValue": "$AMI_ID"},
{"ParameterKey": "SubnetIds", "ParameterValue": "$SUBNET_IDS"},
{"ParameterKey": "SecurityGroupId", "ParameterValue": "$SG_ID"},
{"ParameterKey": "AppVersion", "ParameterValue": "v1"}
]
EOF
手順2: スタックを作る
## スタックを作る
aws cloudformation create-stack --region $AWS_REGION \
--stack-name instance-refresh-verify \
--template-body file://template.yaml \
--parameters file://params.json
## 状態が CREATE_COMPLETE になるまで待つ
aws cloudformation wait stack-create-complete --region $AWS_REGION \
--stack-name instance-refresh-verify
## 作られたサーバーの状態を確認する
aws autoscaling describe-auto-scaling-groups --region $AWS_REGION \
--auto-scaling-group-names instance-refresh-verify-asg \
--query 'AutoScalingGroups[0].Instances[*].[InstanceId,LifecycleState,LaunchTemplate.Version]' \
--output table
v1 のUserDataを積んだサーバーが2台、InService(稼働中)になっていれば成功です。
-------------------------------------------
| DescribeAutoScalingGroups |
+----------------------+-------------+----+
| i-0ce6983c2224596f8 | InService | 1 |
| i-0da3bf0660abdd394 | InService | 1 |
+----------------------+-------------+----+
手順3: 設定を書き換えて入れ替えを起こす
パラメータの AppVersion を v2 に変えて、もう一度スタックを更新します。中身が変わるので LaunchTemplate の新しいバージョンができ、これが入れ替えのきっかけ(トリガー)になります。
sed -i.bak 's/"ParameterValue": "v1"/"ParameterValue": "v2"/' params.json
aws cloudformation update-stack --region $AWS_REGION \
--stack-name instance-refresh-verify \
--template-body file://template.yaml \
--parameters file://params.json
# 状態が Successful になるまで15秒おきに確認する
while true; do
STATUS=$(aws autoscaling describe-instance-refreshes --region $AWS_REGION \
--auto-scaling-group-name instance-refresh-verify-asg \
--query 'InstanceRefreshes[0].[Status,StatusReason]' --output text)
echo "$(date +%T) $STATUS"
echo "$STATUS" | grep -q "^Successful" && break
sleep 15
done
まず新しい v2 のサーバーが2台追加されて一時的に4台になり(MaxHealthyPercentage: 200 で許可した上限)、状態理由が「新しいサーバーの様子見中」から「古いサーバーの停止待ち」に変わりながら、60秒(InstanceWarmup で指定した時間)経ったあとに古い v1 のサーバーが順番に止まっていきました。この間、CloudFormation側は更新中の状態のままです。手元では、更新を始めてからスタックが完了状態になるまで2分半ほどでした。
手順4: 入れ替えないケースも確認する
SkipMatching: true にしてあるので、「入れ替えのきっかけにはなる変更だけど、サーバーの中身は変わらない」場合は入れ替えが起きないはずです。サブネットの並び順だけを逆にして試します。更新の前後でサーバーのIDを比較して、本当に入れ替わっていないかを確認します。
# サブネットの順番を逆にして更新する
BEFORE_IDS=$(aws autoscaling describe-auto-scaling-groups --region $AWS_REGION \
--auto-scaling-group-names instance-refresh-verify-asg \
--query 'AutoScalingGroups[0].Instances[*].InstanceId' --output text)
# SUBNET_IDSの順番を逆にする
SUBNET_IDS_REV=$(echo "$SUBNET_IDS" | awk -F',' '{for(i=NF;i>0;i--){printf "%s", $i; if(i>1) printf ","}; print ""}')
sed -i.bak "s/\"ParameterValue\": \"$SUBNET_IDS\"/\"ParameterValue\": \"$SUBNET_IDS_REV\"/" params.json
aws cloudformation update-stack --region $AWS_REGION \
--stack-name instance-refresh-verify \
--template-body file://template.yaml \
--parameters file://params.json
# 状態が Successful になるまで待つ
aws cloudformation wait stack-update-complete --region $AWS_REGION \
--stack-name instance-refresh-verify
# 更新後のサーバーIDを取得して比較する
AFTER_IDS=$(aws autoscaling describe-auto-scaling-groups --region $AWS_REGION \
--auto-scaling-group-names instance-refresh-verify-asg \
--query 'AutoScalingGroups[0].Instances[*].InstanceId' --output text)
echo "更新前: $BEFORE_IDS"
echo "更新後: $AFTER_IDS"
更新前後で更新前と更新後のIDが完全に一致します。設定変更自体は入れ替えのきっかけになったものの、サーバーの中身がすでに最新と同じだったため、実際の入れ替えはスキップされたということです。
更新前: i-0869f52a814c9f55b i-096b99ddf01f249fc
更新後: i-0869f52a814c9f55b i-096b99ddf01f249fc
手順5: 後片付け
サーバー代がかかり続けないよう、確認が終わったら必ず削除してください。
## スタックを削除する
aws cloudformation delete-stack --region $AWS_REGION \
--stack-name instance-refresh-verify
# 状態が DELETE_COMPLETE になるまで待つ
aws cloudformation wait stack-delete-complete --region $AWS_REGION \
--stack-name instance-refresh-verify
# 不要になったファイルを削除する
rm -f template.yaml params.json params.json.bak
まとめ
ごく小さな構成でしたが、サーバーを一時的に増やしてから入れ替える動きと、中身が同じサーバーは入れ替えをスキップする動きの両方を、実際に手を動かして確認できました。既存の AutoScalingRollingUpdate から、機能を止めずに移行できるのは地味にうれしいポイントかと思います。