AWS Backup+EventBridge+Lambdaによる、タグを使って複数の起動テンプレートが存在する環境でも動的に起動テンプレートのAMIを更新する方法を考えました。
背景
EC2のAMIを取得する方法としてAWS Backupを利用することが多いと思います。私もその一人です。
その際の留意点の一つに、EC2 Auto Scalingに紐づける起動テンプレートのAMIを、EC2のAMI更新に追従させる必要があります。
対処方法はいくつか存在しますが、以下私のニーズを満たすべく手法を検討しました。
・ Backup Job成功時に起動テンプレートの更新をかけたい
・ 複数の起動テンプレートが存在する環境にも対応したい
・ Lambda関数は一つで対応したい
構成
アーキテクチャ
処理フローは以下の通り。
1. AWS BackupでEC2のAMIを取得
2. Backup Job成功をEventBridgeルールで検出しLambdaにEventを送信
3. LambdaはEventに含まれるAMIを取得し、AMIに特定のタグが含まれるかを検出
4. 特定のタグが含まれている場合、同じタグが付与されている起動テンプレートを検索し、AMIを更新
ポイントは対になるEC2と起動テンプレートに共通のタグを付与しておくこと。
- AWS Backupで取得したAMIはEC2のタグを引き継ぐ
- 対になるEC2と起動テンプレートに共通のタグキーを付与し、タグ値はペアごとに変えることで複数の起動テンプレートがある環境においても動的に対応することが可能
Lambda関数コード
Python3.12でLambda関数コードを作成しています。
import json
import boto3
ec2 = boto3.client('ec2')
# Backupキーが存在するか判定する関数
def check_backup_tag(tags):
# Keyが'Backup'のタグを探す
backup_tag = next((tag for tag in tags if tag['Key'] == 'Backup'), None)
# タグ存在有無判定
if backup_tag:
print(f"Found Backup tag: {backup_tag['Value']}")
return backup_tag['Value']
else:
print("No Backup tag found.")
return None
# 起動テンプレートIDを取得する関数
def get_launchtemplate_id(tag_value):
try:
# 起動テンプレート情報取得
lt = ec2.describe_launch_templates(
Filters=[
{
'Name': 'tag:Backup',
'Values': [
tag_value
]
}
]
)
# 起動テンプレートID取得
lt_id = lt['LaunchTemplates'][0]['LaunchTemplateId']
print(f"Found Launch Template ID: {lt_id}")
return lt_id
except Exception as e:
print(f"No launch template found with tag 'Backup' and value '{tag_value}'.")
return None
def lambda_handler(event, context):
try:
# イベントからAMI ARNを取得
ami_arn = event['resources'][0]
# ARNからAMI IDを取得
ami_id = ami_arn.split('/')[-1]
print(f"Extracted AMI ID: {ami_id}")
# AMIのタグを取得
response = ec2.describe_images(ImageIds=[ami_id])
tags = response['Images'][0]['Tags']
# AMIにBackupキーが含まれるか判定
exist_tag = check_backup_tag(tags)
# Backupキーがなければ処理終了
if exist_tag == None:
print("Terminating process due to missing Backup tag.")
return {"status": "terminated", "reason": "Missing Backup tag"}
# 起動テンプレートID取得
lt_id = get_launchtemplate_id(exist_tag)
# 起動テンプレートIDを取得できなければ処理終了
if lt_id == None:
print("Terminating process due to missing lt ID.")
return {"status": "terminated", "reason": "Missing lt ID"}
# 現バージョンの説明文を取得
description_response = ec2.describe_launch_template_versions(
LaunchTemplateId = lt_id
)
description = description_response['LaunchTemplateVersions'][0].get('VersionDescription', '')
# 新しいバージョンの起動テンプレートを作成
create_response = ec2.create_launch_template_version(
LaunchTemplateId = lt_id,
SourceVersion = '$Latest',
VersionDescription = description,
LaunchTemplateData = {
'ImageId': ami_id
}
)
print(f"Created new launch template version: {create_response['LaunchTemplateVersion']['VersionNumber']}")
# 作成した起動テンプレートをデフォルトバージョンに設定
modify_response = ec2.modify_launch_template(
LaunchTemplateId = lt_id,
DefaultVersion = '$Latest'
)
default_version = modify_response['LaunchTemplate']['DefaultVersionNumber']
print(f"Set new default version: {default_version}")
# 最新の2つ前のバージョンを削除
latest_version = modify_response['LaunchTemplate']['LatestVersionNumber']
previous_version = int(latest_version) - 2
if previous_version < 1:
print(f"No valid versions to delete. Calculated previous_version: {previous_version}")
else:
# 既存のバージョンを確認
try:
version_response = ec2.describe_launch_template_versions(
LaunchTemplateId=lt_id,
Versions=[str(previous_version)]
)
if version_response['LaunchTemplateVersions']:
ec2.delete_launch_template_versions(
LaunchTemplateId=lt_id,
Versions=[str(previous_version)]
)
print(f"Deleted old version: {previous_version}")
else:
print(f"Version {previous_version} does not exist, skipping deletion.")
except Exception as e:
print(f"Error while checking or deleting version {previous_version}: {e}")
# 正常終了メッセージ
print("Process completed successfully.")
return {"status": "success", "message": "Process completed successfully", "new_default_version": default_version}
except Exception as e:
print(f"Error in Lambda handler: {e}")
return {"status": "error", "reason": str(e)}
やってみる
検証環境はGitHubで公開しています。
実行前確認
まず実行前のEC2、起動テンプレートのタグ設定および起動テンプレートのAMIIDを確認します。
オンデマンドバックアップ実行
次にバックアッププランからオンデマンドバックアップを作成します。
作成後数分経つと、作成したオンデマンドバックアップのジョブが完了します。
バックアップジョブが完了すると、AWS Backupから以下のイベントが発生します。
{
"version": "0",
"id": "cbe48d40-34f7-1206-c0c3-624561a2c6bb",
"detail-type": "Backup Job State Change",
"source": "aws.backup",
"account": "XXXXXXXXXXXX",
"time": "2025-01-12T12:56:43Z",
"region": "ap-northeast-1",
"resources": [
"arn:aws:ec2:ap-northeast-1::image/ami-0f3fa8eafd368dfbd"
],
"detail": {
"backupJobId": "432862c8-a8bd-41ae-abca-c9c971b2bdb8",
"backupSizeInBytes": "32212254720",
"backupVaultArn": "arn:aws:backup:ap-northeast-1:XXXXXXXXXXXX:backup-vault:BackupVault",
"backupVaultName": "BackupVault",
"bytesTransferred": "0",
"creationDate": "2025-01-12T12:48:27.744Z",
"iamRoleArn": "arn:aws:iam::XXXXXXXXXXXX:role/AWSBackupRole",
"resourceArn": "arn:aws:ec2:ap-northeast-1:XXXXXXXXXXXX:instance/i-044cc17d6e43adff6",
"resourceType": "EC2",
"state": "COMPLETED",
"completionDate": "2025-01-12T12:52:34.021600696Z",
"startBy": "2025-01-12T13:48:27.744Z",
"percentDone": 0
}
}
上記イベントをEventBridgeルールのイベントパターンで検出し、検出したイベントをLambdaに送信し処理します。
{
"detail-type": ["Backup Job State Change"],
"source": ["aws.backup"],
"detail": {
"state": ["COMPLETED"]
}
}
実行後確認
最後に実行結果を確認します。
完了したバックアップジョブの詳細から取得したAMIIDを確認します。
起動テンプレートのAMIIDと比較し、取得したAMIIDに更新されていることが確認できます。
併せてバージョンも最新のものに更新されていることも確認できます。
Auto Scaling側で起動テンプレートの最新を利用する設定になっていれば、スケールアウトも問題なく最新のAMIを利用したEC2が起動してきます。
まとめ
AWS Backupジョブの成功を契機に起動テンプレートのAMIを更新する方法を試しました。
「対になるEC2と起動テンプレートに共通のタグキーを付与」「Backupジョブで取得したAMIはEC2のタグを引き継ぐ」ことを活かすことで起動テンプレートが複数存在する環境でも動的に対応できます。
今回はAWS Backupで検討しましたが、EventBridgeルールのイベントパターンをAMIが更新されたことを検出するようにすれば、DLMを利用している環境でも利用できると思います。
こちらの記事が少しでも誰かの役に立てば幸いです。