はじめに
ほうき星です。
AutoScalingGroupを使用し最小:1、最大:1のAutoHealingを構成する場合、起動中のインスタンスからAMIを取得し起動テンプレートやAutoScalingGroupの設定を更新する運用を行うことがあります。
この更新作業はLambda関数等を使用し自動化されることが多いですが、CloudFormationを使用してリソースを定義している場合は少し工夫しないとドリフトが発生します。
本記事ではドリフトを回避しつつAMIを更新する運用方法を紹介します。
AutoHealing構成時のAMI設定の更新運用
AutoHealing構成(特に最小:1、最大:1)の時に新しく起動してくるインスタンスを最新の状態に保つため、起動中のインスタンスからAMIを取得し起動テンプレートやAutoScalingGroupの設定を更新する運用を行うことがあります。
この運用では、CloudFormationテンプレートでAutoHealing構成を定義している場合に、次の課題が発生します。
課題:AMIの更新によるCloudFormationスタックのドリフト
CloudFormationはAutoScalingGroupの起動テンプレートバージョンの指定において$Latest
や$Default
を使用することはできません。
そのため前述の運用において取得したAMIで新しい起動テンプレートバージョンを作成するだけではなく、AutoScalingGroupが参照する起動テンプレートのバージョン指定も併せて変更する必要があり、これによりドリフトが発生します。
※起動テンプレートはCloudFormation外で新しいバージョンが作成されてもドリフトを検知しません
解決策
パターン①:SSMパラメータを活用する
起動テンプレートのAMI指定ではAMIを直接指定する他にSSMパラメータを使用することができます。
これを利用しCloudFormationテンプレート外で定義したSSMパラメータを起動テンプレートで指定し、AMI取得後にSSMパラメータをLambda等を用いて更新することで、起動テンプレート及びAutoScalingGroupの設定を変更することなく(=ドリフトさせずに)運用することが可能です。
SSMパラメータ(AWS::SSM::Parameter)をCloudFormationテンプレート内で定義し、それをLambda等で更新してしまうとSSMパラメータ部分がドリフトしてしまいます。
必ずCloudFormationテンプレート外で事前に定義したものを更新します。
CloudFormationテンプレートサンプル
以下はSSMパラメータを使用するAuto Healing構成を定義するCloudFormationテンプレート及びAWSBackupのCOMPLETED
イベントをトリガーにSSMパラメータを作成したAMIで更新するサンプルです。
AWSTemplateFormatVersion: 2010-09-09
Description: Stack of solution 1
Parameters:
IamInstanceProfileArn:
Type: String
Description: Enter the arn of the instance profile to be used for the instance.
ImageIdParameterName: # CloudFormationテンプレート外で定義したSSMパラメータ名を指定する
Type: AWS::SSM::Parameter::Name
Description: Enter the parameter name of the parameter store where you filled in the ImageId.
SecurityGroupIds:
Type: List<AWS::EC2::SecurityGroup::Id>
Description: Enter the security group ids to be used for the instance.
VPCZoneIdentifier:
Type: List<AWS::EC2::Subnet::Id>
Description: Enter the subnet ids to be used for the instance.
Resources:
Solution1LaunchTemplate: # 起動テンプレート
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateName: solution1-launch-template
LaunchTemplateData:
IamInstanceProfile:
Arn: !Ref IamInstanceProfileArn
ImageId: !Sub resolve:ssm:${ImageIdParameterName} # SSMパラメータから取得
InstanceType: t2.micro
SecurityGroupIds:
!Ref SecurityGroupIds
TagSpecifications:
- ResourceType: instance
Tags:
- Key: Name
Value: solution1-instance
- Key: ImageIdParameterName
Value: !Ref ImageIdParameterName
Solution1AutoScalingGroup: # AutoScalingGroup(Auto Healing)
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
AutoScalingGroupName: solution1-auto-scaling-group
LaunchTemplate:
LaunchTemplateId: !Ref Solution1LaunchTemplate
Version: !GetAtt Solution1LaunchTemplate.LatestVersionNumber
MaxSize: 1
MinSize: 1
VPCZoneIdentifier:
!Ref VPCZoneIdentifier
-
solution1.yml
テンプレートは事前準備のSSMパラメータ名をパラメータImageIdParameterName
として定義 - 起動テンプレートの
ImageId
プロパティでは!Sub resolve:ssm:${ImageIdParameterName}
とすることでSSMパラメータを指定
AWSBackupをトリガーにSSMパラメータを更新するServerlessFunctionサンプル
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Lambda function to update SSM Parameter Store with AMI ID after AWS Backup completes.
Resources:
UpdateImageIdInParameterStoreFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: UpdateImageIdInParameterStore
Runtime: python3.12
Handler: lambda_function.lambda_handler
MemorySize: 128
Timeout: 30
Policies:
- AWSLambdaBasicExecutionRole
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- ec2:DescribeImages
- ssm:PutParameter
Resource: "*"
Events:
BackupCompletedEvent:
Type: EventBridgeRule
Properties:
Pattern:
source:
- "aws.backup"
detail-type:
- "Backup Job State Change"
detail:
state:
- "COMPLETED"
import boto3
def lambda_handler(event, context):
print(f"{event=}")
# AWSBackupイベントから、ImageIdを取得
image_id: str = event["resources"][0].split("/")[-1]
# Imageに付与された'ImageIdParameterName'タグを取得
describe_images_response: dict = boto3.client("ec2").describe_images(ImageIds=[image_id])
specific_tag: list[str] = [
tag["Value"]
for tag in describe_images_response["Images"][0]["Tags"]
if tag["Key"] == "ImageIdParameterName"
]
if not specific_tag:
print("Tag: ImageIdParameterName is not found, Skipped.")
return
parameter_name: str = specific_tag[0]
# 取得したタグを元に、SSMパラメータにImageIdを書き込む
boto3.client("ssm").put_parameter(
Name=parameter_name,
Value=image_id,
Overwrite=True
)
print(f"ImageId: {image_id} is written to SSM Parameter Store: {parameter_name}")
- AWSBackupの完了をトリガーにLambda関数を実行
- 作成されたAMIに特定のタグ:ImageIdParameterName が存在するかチェック、存在すれば値として設定されたSSMパラメータを作成されたAMIで更新
AWSBackup後の動作結果
- AWSBackupの完了をトリガーにLambda関数が動作、SSMパラメータを更新
- CloudFormationテンプレート外で作成したSSMパラメータを更新しただけなので、ドリフト無し
パターン②:AMIをCloudFormationのパラメータとして定義し、スタックの更新を行う
CloudFormationのパラメータとしてImageId
を定義し、AMI取得後にLambda等を使用しImageId
パラメータに最新のAMIを指定しスタックを更新することで、こちらも(当たり前ですが)ドリフトなく運用することができます。
1つのテンプレートを使用して複数のAutoHealing構成を定義している場合、スタック更新のタイミング等を考慮・検討する必要があります。
CloudFormationテンプレートサンプル
AWSTemplateFormatVersion: '2010-09-09'
Description: Stack of solution2
Parameters:
IamInstanceProfileArn:
Type: String
Description: Enter the arn of the instance profile to be used for the instance.
ImageId:
Type: AWS::EC2::Image::Id
Description: Enter the image id to be used for the instance.
SecurityGroupIds:
Type: List<AWS::EC2::SecurityGroup::Id>
Description: Enter the security group ids to be used for the instance.
VPCZoneIdentifier:
Type: List<AWS::EC2::Subnet::Id>
Description: Enter the subnet ids to be used for the instance.
Resources:
Solution2LaunchTemplate: # 起動テンプレート
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateName: solution2-launch-template
LaunchTemplateData:
IamInstanceProfile:
Arn: !Ref IamInstanceProfileArn
ImageId: !Ref ImageId # CloudFormationパラメータを指定
InstanceType: t2.small
SecurityGroupIds:
!Ref SecurityGroupIds
TagSpecifications:
- ResourceType: instance
Tags:
- Key: Name
Value: solution1-instance
Solution2AutoScalingGroup: # AutoScalingGroup(Auto Healing)
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
AutoScalingGroupName: solution2-auto-scaling-group
LaunchTemplate:
LaunchTemplateId: !Ref Solution2LaunchTemplate
Version: !GetAtt Solution2LaunchTemplate.LatestVersionNumber
MaxSize: 1
MinSize: 1
VPCZoneIdentifier:
!Ref VPCZoneIdentifier
-
solution2.yml
テンプレートはパラメータImageId
を定義 - 起動テンプレートの
ImageId
プロパティでは!Ref ImageId
としてパラメータImageId
を指定
AWSBackupをトリガーにスタックを更新するServerlessFunctionサンプル
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Lambda function to update CloudFormation stack's ImageId parameter after AWS Backup AMI completed.
Resources:
UpdateStackImageIdFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: UpdateStackImageIdFunction
Runtime: python3.12
Handler: lambda_function.lambda_handler
MemorySize: 128
Timeout: 30
Policies:
- AWSLambdaBasicExecutionRole
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- ec2:DescribeImages
- cloudformation:DescribeStacks
- cloudformation:UpdateStack
Resource: "*"
Events:
BackupCompletedEvent:
Type: EventBridgeRule
Properties:
Pattern:
source:
- "aws.backup"
detail-type:
- "Backup Job State Change"
detail:
state:
- "COMPLETED"
import boto3
def lambda_handler(event, context):
print(f"{event=}")
# AWSBackupイベントから、ImageIdを取得
image_id: str = event["resources"][0].split("/")[-1]
# Imageに付与された'StackName'タグを取得
describe_images_response: dict = boto3.client("ec2").describe_images(ImageIds=[image_id])
specific_tag: list[str] = [
tag["Value"]
for tag in describe_images_response["Images"][0]["Tags"]
if tag["Key"] == "StackName"
]
if not specific_tag:
print("Tag: StackName is not found, Skipped.")
return
stack_name: str = specific_tag[0]
# スタックの現在パラメータを取得
cfn = boto3.client("cloudformation")
stack: dict = cfn.describe_stacks(StackName=stack_name)["Stacks"][0]
current_parameters: list[dict] = stack.get("Parameters", [])
# パラメータを更新するための新しいリストを作成
new_parameters: list[dict] = []
image_id_updated: bool = False
for param in current_parameters:
if param["ParameterKey"] == "ImageId":
new_parameters.append({
"ParameterKey": "ImageId",
"ParameterValue": image_id
})
image_id_updated = True
else:
# 変更しないパラメータは「UsePreviousValue: True」で指定
new_parameters.append({
"ParameterKey": param["ParameterKey"],
"UsePreviousValue": True
})
if not image_id_updated:
print(f"Parameter: ImageId not found in stack {stack_name}. Skipped update.")
return
# スタック更新を実行
response: dict = cfn.update_stack(
StackName=stack_name,
UsePreviousTemplate=True,
Parameters=new_parameters,
Capabilities=["CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND"]
)
print(f"Stack update started: {response["StackId"]}")
- AWSBackupの完了をトリガーにLambda関数を実行
- 作成されたAMIに特定のタグ:StackName が存在するかチェック、存在すれば値として設定されたスタックのImageIdパラメータを作成されたAMIでスタックを更新
さいごに
この記事ではドリフトを回避しつつAMIを更新する方法を2つ紹介しました。
ドリフトを発生させることなく柔軟な運用を実現するためのテクニックの一つとして誰かの役に立てば幸いです。