Lambda(python)でAWS Backupの最新イメージ(AMI)情報をパラメータストアへ登録する(EC2スポットインスタンス自動化1)

# 背景
個人向けに最低費用で運用するため、Ec2をスポットインスタンスで使用しているが、時々スポットリクエストが中断される。その都度、再度スポットリクエストで立ち上げなおしているが、この処理を自動化したい。

# 大まかな流れ
スポットインスタンスの中断はイベント通知されるので、それをトリガとして、立ち上げ処理を行う。

### ①普段の処理

1. 起動時に使用するAMIの最新IDを取得しておく。
    1. バックアップ(AWS Backup)でAMIが作成された際、CreateImageイベントが発生。
    1. EventBridgeでイベントを拾い、Lambda関数にて該当のAMIのIDを取得し、ParameterStoreへ保存しておく。

### ②中断イベント発生時の流れ

1. スポットインスタンスの中断イベントが通知される。
1. EventBridgeでイベントを拾う。
1. イベントに連動して、Step Functionsへ連携。
1. Step Functions内で以下の流れを作る。
    1. ParameterStoreから最新のAMI IDを取得し、スポットリクエストを発行。(Lambda関数)
        1. 起動状況を確認。
        1. ダメだったら、AZやインスタンスタイプを変更して再トライ。
    1. 起動に成功したか判定し、成功していればElastic IPを付け替える。(Lambda関数)
    1. 処理結果を通知。(LINE通知)
※AZとインスタンスタイプのパターンはリスト化して持っておく。(ParameterStore)

# 記事の全体
要素に分けて全5回で記載していきます。
1. [AWS BackupでEC2のAMIが作成されるので、その最新のAMI IDをイベントから取得してParameterStoreに登録する。](https://qiita.com/silv224/items/0c1d157d34cd8ffb651b)**←今回の内容**

1. [AWS Lambda(python)でLINEへ通知する。](https://qiita.com/silv224/items/37453c8e7401f3a77d5f)
1. [AWS Lambda(python)でEC2スポットリクエストを実行する。](https://qiita.com/silv224/items/e089df858dc518a99713)
1. [AWS Lambda(Python,boto3)でElasticIPを付け替える。](https://qiita.com/silv224/items/5e5d4b07a92aa10d1bee)
1. [Step FunctionでLambdaでスポットリクエストを実行し、ElasticIPを付け替える。](https://qiita.com/silv224/items/1d8f425eca0f31db7236)

# 調べたこと
1. boto3でAMI IDを取得する方法。
https://qiita.com/handa3/items/8ea3ee59145ccf55d525

2. そもそもAWS BackupでAMIが作成された際に、CreateImageが通知されるので、EventBridge経由でLambdaを起動し、ParameterStoreに保存すればよい。
https://zenn.dev/chittai/articles/20210325-update-ps-for-dr

2.の方法がよさそうなので、そちらで進める。

[参考]

https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/monitor-ami-events.html

BackupにてAMIが作成されると、CreateImageのEventが発行される。
```(Eventのサンプル).json
{
    "version": "0",
    "id": "*******-*****-****-****-*********",
    "detail-type": "EC2 AMI State Change",
    "source": "aws.ec2",
    "account": "アカウントIDの番号",
    "time": "2023-08-05T03:34:14Z",
    "region": "ap-northeast-1",
    "resources": [
        "arn:aws:ec2:ap-northeast-1::image/ami-********"
    ],
    "detail": {
        "RequestId": "********-****-****-****-**********",
        "State": "available",
        "ImageId": "ami-*********",
        "ErrorMessage": ""
    }
}
```

AMIのIDは取れるものの、どのインスタンスのAMIなのかが判別できない。
AWS Backupで、AMIのタグにあるNameに取得元インスタンス名称を入れているので、それを取得する。

AMI情報取得にはアカウントIDを指定するが、以下の方法でコードに記載することなく取得可能。

https://dev.classmethod.jp/articles/get-aws-account-id-and-region-name-from-boto3/

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2/client/describe_images.html

```(AMI情報サンプル).json
{
    "Images": [
        {
            "Architecture": "x86_64",
            "CreationDate": "2023-08-05T04:05:47.000Z",
            "ImageId": "ami-***********",
            "ImageLocation": "*********/AwsBackup_i-*************_********-****-****-****-******",
            "ImageType": "machine",
            "Public": false,
            "OwnerId": "*********",
            "PlatformDetails": "Linux/UNIX",
            "UsageOperation": "RunInstances",
            "ProductCodes": [
                {
                    "ProductCodeId": "aw0evgkw8e5c1q413zgy5pjce",
                    "ProductCodeType": "marketplace"
                }
            ],
            "State": "available",
            "BlockDeviceMappings": [
                {
                    "DeviceName": "/dev/sda1",
                    "Ebs": {
                        "DeleteOnTermination": true,
                        "SnapshotId": "snap-********",
                        "VolumeSize": 20,
                        "VolumeType": "gp2",
                        "Encrypted": true
                    }
                }
            ],
            "Description": "This image is created by the AWS Backup service.",
            "EnaSupport": true,
            "Hypervisor": "xen",
            "Name": "AwsBackup_i-*************_********-****-****-****-******",
            "RootDeviceName": "/dev/sda1",
            "RootDeviceType": "ebs",
            "SriovNetSupport": "simple",
            "Tags": [
                {
                    "Key": "Name",
                    "Value": "EC2のホスト名を入れておく"
                },
                {
                    "Key": "aws:backup:source-resource",
                    "Value": "i-********:*******_********-****-****-****-******"
                },
                {
                    "Key": "AWSBackup",
                    "Value": "EC2"
                }
            ],
            "VirtualizationType": "hvm"
        }
    ]
```

# CreateImageイベント受信、ParameterStoreの登録用Lambda
```(Lambda関数 getBackupAmiId).py
import boto3
import logging
import json

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Region
REGION = 'ap-northeast-1'
HOSTNAME = 'EC2のTagsのNameに登録しているホスト名'

def lambda_handler(event, context):
    logger.info('event[' + json.dumps(event) + ']')
    image_id = event['detail']['ImageId']

    ec2 = boto3.client('ec2', region_name=REGION)
    # アカウントIDを取得
    Owner_id = boto3.client('sts').get_caller_identity().get('Account')

    response = ec2.describe_images(
        Owners = [Owner_id],
        ImageIds = [image_id]
    )
    logger.info('image info[' + json.dumps(response) + ']')

    ssm = boto3.client('ssm', region_name=REGION)
    for idx, keyvalue in enumerate(response['Images'][0]['Tags']):

        # TagsのNameがホスト名と一致しているかチェック
        if( keyvalue['Key'] == 'Name' and keyvalue['Value'] == HOSTNAME ):

            # パラメータストアへAMIのimageIDを保存
            response_put =  ssm.put_parameter(
                Name = '/EC2Backup/latestAMI/' + HOSTNAME,
                Value = image_id,
                Type = 'String',
                Overwrite = True
            )
        #
        if( keyvalue['Key'] == 'aws:backup:source-resource' ):

            # パラメータストアへEC2インスタンスIDを保存
            response_put =  ssm.put_parameter(
                Name = '/EC2Backup/latestAMI/source-resource' ,
                Value = keyvalue['Value'].split(':')[0] ,
                Type = 'String',
                Overwrite = True
            )
```
Lambdaのロールには、AWSLambdaBasicExecutionRoleと、AMI情報取得、ParameterStoreの操作に必要そうな許可ポリシーを入れた。(合っている？)
```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeImages",
                "ssm:PutParameter",
                "sts:AssumeRole",
                "ssm:GetParameter"
            ],
            "Resource": "*"
        }
    ]
}
```

# EventBridgeへの登録方法
https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/monitor-ami-events.html

ほぼデフォルト設定。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309202/13bdc368-da24-2571-0129-830d7834acc3.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309202/af843d27-b6c1-8df7-b900-31df2b229320.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309202/a74e0f2b-25b3-3a94-004d-081c8e5a81a3.png)
```(イベントパターン).json
{
  "source": ["aws.ec2"],
  "detail-type": ["EC2 AMI State Change"],
  "detail": {
    "State": ["available"]
  }
}
```

Eventを受信したら、用意したLambdaが実行されるようにターゲットを指定。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309202/b9a75c4b-b710-90bd-9879-73b29da88f5d.png)

# ParameterStoreへの登録確認
AWS Backupでバックアップが取得されると、イベントから連動してパラメータストアに登録されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309202/3f33b585-8089-50bb-5afe-57053900e3fa.png)