AWS BackupでAMI作成成功時に自動で起動テンプレートを更新

![001samune.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3984127/40c58cf3-2df8-61e7-8acd-a24884ce889e.png)

AWS Backup+EventBridge+Lambdaによる、タグを使って複数の起動テンプレートが存在する環境でも動的に起動テンプレートのAMIを更新する方法を考えました。


# 背景
EC2のAMIを取得する方法としてAWS Backupを利用することが多いと思います。私もその一人です。
その際の留意点の一つに、EC2 Auto Scalingに紐づける起動テンプレートのAMIを、EC2のAMI更新に追従させる必要があります。
対処方法はいくつか存在しますが、以下私のニーズを満たすべく手法を検討しました。
```
・ Backup Job成功時に起動テンプレートの更新をかけたい
・ 複数の起動テンプレートが存在する環境にも対応したい
・ Lambda関数は一つで対応したい
```

# 構成
### アーキテクチャ
![config.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3984127/da7e3e57-83d1-3023-1cc7-e755ed0906a5.png)

処理フローは以下の通り。
```
1. AWS BackupでEC2のAMIを取得
2. Backup Job成功をEventBridgeルールで検出しLambdaにEventを送信
3. LambdaはEventに含まれるAMIを取得し、AMIに特定のタグが含まれるかを検出
4. 特定のタグが含まれている場合、同じタグが付与されている起動テンプレートを検索し、AMIを更新
```

ポイントは対になるEC2と起動テンプレートに共通のタグを付与しておくこと。
![画像4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3984127/04386f8b-2763-72db-1133-edd2b78543bc.png)

:::note info
* AWS Backupで取得したAMIはEC2のタグを引き継ぐ
* 対になるEC2と起動テンプレートに共通のタグキーを付与し、タグ値はペアごとに変えることで複数の起動テンプレートがある環境においても動的に対応することが可能
:::



### Lambda関数コード
Python3.12でLambda関数コードを作成しています。
```python:lt-update.py
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で公開しています。

https://github.com/tyskJ/aws-backup-update-launchtemplate

### 実行前確認
まず実行前のEC2、起動テンプレートのタグ設定および起動テンプレートのAMIIDを確認します。
![画像5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3984127/f39edaa2-b833-0b4b-839c-224111477f50.png)
![画像6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3984127/f9095f2a-ce54-a2c4-000b-bb65967b0829.png)
![画像7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3984127/e4bb8a56-0110-ea24-1b97-2556618ce4ec.png)


### オンデマンドバックアップ実行
次にバックアッププランからオンデマンドバックアップを作成します。
![画像9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3984127/c17270da-9754-65e7-cb69-27cd43878259.png)
![画像10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3984127/ffd1e0d8-fdee-1681-2660-bbfdfac29a23.png)
![画像11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3984127/cb583f4e-67bf-7102-2b7f-368256dddb9e.png)

作成後数分経つと、作成したオンデマンドバックアップのジョブが完了します。
![画像12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3984127/37e88692-2e9a-22b3-1d2c-fcb8b247ba66.png)

バックアップジョブが完了すると、AWS Backupから以下のイベントが発生します。
```json:event
{
    "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に送信し処理します。
```json:event-pattern
{
  "detail-type": ["Backup Job State Change"],
  "source": ["aws.backup"],
  "detail": {
    "state": ["COMPLETED"]
  }
}
```

### 実行後確認
最後に実行結果を確認します。
完了したバックアップジョブの詳細から取得したAMIIDを確認します。
![画像13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3984127/8ea5b7fc-2a8c-ff85-62a1-6eeaf33886f9.png)

起動テンプレートのAMIIDと比較し、取得したAMIIDに更新されていることが確認できます。
併せてバージョンも最新のものに更新されていることも確認できます。
Auto Scaling側で起動テンプレートの最新を利用する設定になっていれば、スケールアウトも問題なく最新のAMIを利用したEC2が起動してきます。
![画像14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3984127/55808f2c-dd62-6357-30e4-1b24a943e973.png)


# まとめ
AWS Backupジョブの成功を契機に起動テンプレートのAMIを更新する方法を試しました。
「対になるEC2と起動テンプレートに共通のタグキーを付与」「Backupジョブで取得したAMIはEC2のタグを引き継ぐ」ことを活かすことで起動テンプレートが複数存在する環境でも動的に対応できます。
今回はAWS Backupで検討しましたが、EventBridgeルールのイベントパターンをAMIが更新されたことを検出するようにすれば、DLMを利用している環境でも利用できると思います。
こちらの記事が少しでも誰かの役に立てば幸いです。


# 参考文献
### リファレンス
* [Boto3 1.35.97 documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html)
### ブログ
* [特定タグが設定されたAMIが作成された時に自動で起動テンプレートを更新する](https://dev.classmethod.jp/articles/use-daily-backup-of-asg-instance-for-golden-image/#toc-)