AMI を定期的に取得して世代管理する


※ <font color="red">2020/2/25 追記</font>
2020年1月、ようやく EC2 の バックアップ/リストア を支援する [AWS Backup EC2 が リリース](https://aws.amazon.com/jp/about-aws/whats-new/2020/01/aws-backup-adds-support-amazon-elastic-cloud-compute-instance-backup/)された。
私も、これまでこの自作バックアップを使っていたけど、AWS Backup EC2 へ移行した。
[AWS Backup EC2 を実運用へ導入したときのあれやこれや](https://qiita.com/tonishy/items/2263c237d3833113d817)





## 概要
* EC2タグ "AMI-Backup-Generation = n(世代数)" が付いている EC2インスタンスのイメージ (AMI+Snapshot) を定期的に作成する。
* AMIの作成と登録解除を行なうLambda (関数名： ec2-image-backup)と、登録解除されたAMIのスナップショットを削除するLambda（関数名：delete_snapshot_after_ami_deregister）の２つを作成して動かす。
* 設定した世代数よりも古くなった AMI は Lambda関数 ec2-image-backup によってAMIの登録は解除されるが、その AMI の構成要素である スナップショット は残ったままになる。そこで、AMIの登録解除が行われたら（DeregisterImage の AWS API Callが呼び出されたら）、その AMI ID に紐づくすべてのスナップショットを削除する Lambda関数 delete_snapshot_after_ami_deregister を実行する。

## 参考にさせてもらったページ
[EC2のスナップショットを自動的にAWS Lambdaで作成する](https://qiita.com/HorieH/items/66bb68d12bd8fdbbd076
)
[AMI削除時にスナップショットも自動で削除するCloudWatch Events](https://dev.classmethod.jp/cloud/aws/ami-and-snapshot-delete/)

# **AMIの作成と世代管理**
## 設定
Lambda に付けるロールを作成する。

### IAM ポリシー
ロールにアタッチするポリシーを Policy Generator で作成する

①ポリシー名：AmazonEC2DeregisterImage
>効果：許可
AWS サービス：Amazon EC2
アクション：EC2:DeregisterImage
Amazon リソースネーム (ARN)：*

②ポリシー名：AmazonEC2CreateImage
>効果：許可
AWS サービス：Amazon EC2
アクション：EC2:CreateImage
Amazon リソースネーム (ARN)：*

③ポリシー名：AmazonEC2DescribeImages
>効果：許可
AWS サービス：Amazon EC2
アクション：EC2:DescribeImages
Amazon リソースネーム (ARN)：*

④ポリシー名：AmazonEC2DescribeInstances
>効果：許可
AWS サービス：Amazon EC2
アクション：EC2:DescribeInstances
Amazon リソースネーム (ARN)：*

### IAM ロール
ロール名：ec2-image-backup
>trusted entity：Lambda
アタッチするポリシー：
　AmazonEC2DeregisterImage
　AmazonEC2CreateImage
　AmazonEC2DescribeImages
　AmazonEC2DescribeInstances
　AWSLambdaBasicExecutionRole

## Lambda
### Labmda関数を一から作成
関数名： ec2-image-backup
>ロール：既存のロールを選択
既存のロール：ec2-image-backup
コードエントリ タイプ：コードをインラインで編集
ランタイム：Python 3.6
ハンドラ 情報：lambda_function.lambda_handler
環境変数：
　キー：AWS_ACCOUNT, 値：AWSアカウントID（12桁の数字）
基本設定：
　タイムアウト：5分

コード

```python
import os
import boto3
import collections
from time import sleep
from datetime import datetime, timedelta
from botocore.client import ClientError
from logging import getLogger, INFO
 
logger = getLogger()
 
ec2_client = boto3.client('ec2')
 
def lambda_handler(event, context):
    descriptions = create_image()
    delete_old_images(descriptions)
 
def create_image():
    instances = get_instances(['AMI-Backup-Generation'])
    descriptions = {}
    for instance in instances:
        tags = { tag['Key']: tag['Value'] for tag in instance['Tags'] }
        generation = int( tags.get('AMI-Backup-Generation', 0) )
 
        if generation < 1:
            continue
 
        instance_id = instance.get('InstanceId')
        create_data_jst = (datetime.now() + timedelta(hours=9)).strftime("%Yy%mm%dd_%Hh%Mm%Ss")
        ami_name = '%s_%s' % (tags['Name'], instance_id)
        ami_name = ami_name + "_" + create_data_jst
        description = instance_id
 
        image_id = _create_image(instance_id, ami_name, description)
        logger.info('Create Image: ImageId:%s (%s) ' % (image_id['ImageId'], ami_name))
        print('Create Image: ImageId:%s (%s) ' % (image_id['ImageId'], ami_name))       
        descriptions[description] = generation
 
    return descriptions
     
def get_instances(tag_names):
    reservations = ec2_client.describe_instances(
        Filters=[
            {
                'Name': 'tag-key',
                'Values': tag_names
            }
        ]
    )['Reservations']
 
    return sum([
        [instance for instance in reservation['Instances']]
        for reservation in reservations
    ], [])
 
def _create_image(instance_id, ami_name, description):
    for i in range(1, 3):
        try:
            return ec2_client.create_image(
                Description = description,
                NoReboot = True,
                InstanceId = instance_id,
                Name = ami_name
                )
        except ClientError as e:
            logger.exception(str(e))
            print(str(e))
        sleep(2)
    raise Exception('cannot create image ' + ami_name)
 
def delete_old_images(descriptions):
    images_descriptions = get_images_descriptions(list(descriptions.keys()))
    for description, images in images_descriptions.items():
        delete_count = len(images) - descriptions[description]
        if delete_count <= 0:
            continue
 
        images.sort(key=lambda x:x['CreationDate'])
        old_images = images[0:delete_count]
 
        for image in old_images:
            _deregister_image(image['ImageId'])
            logger.info('Deregister Image: ImageId:%s (%s)' % (image['ImageId'], image['Description']))
            print('Deregister Image: ImageId:%s (%s)' % (image['ImageId'], image['Description']))
 
def get_images_descriptions(descriptions):
    images = ec2_client.describe_images(
        Owners = [
            os.environ['AWS_ACCOUNT']
        ],
        Filters = [
            {
                'Name': 'description',
                'Values': descriptions,
            }
        ]
    )['Images']
 
    groups = collections.defaultdict(lambda: [])
    { groups[ image['Description'] ].append(image) for image in images }
 
    return groups
 
def _deregister_image(image_id):
    for i in range(1, 3):
        try:
            return ec2_client.deregister_image(
                ImageId = image_id
            )
        except ClientError as e:
            logger.exception(str(e))
            print(str(e))
        sleep(2)
    raise Exception('Cannot Deregister image: ' + image_id)
```

## CloudWatch イベント
### ルール作成
イベントソース：スケジュール
>Cron 式：　30 0,6,12,18 * * ? *

　↑は、グリニッジ標準時間で設定する。
　この設定で、日本時間の毎日 3:30, 9:30, 15:30, 21:30にイベントが実行される

ターゲット
>Lambda関数：ec2-image-backup

# **登録が解除されたAMIの構成要素だったスナップショットを削除する**
## 設定
Lambda に付けるロールを作成する。

### IAM ポリシー
ロールにアタッチするポリシーを Policy Generator で作成する

①ポリシー名：AmazonEC2DescribeSnapshots
>効果：許可
AWS サービス：Amazon EC2
アクション：EC2:DescribeSnapshots
Amazon リソースネーム (ARN)：*

②ポリシー名：AmazonEC2DeleteSnapshot
>効果：許可
AWS サービス：Amazon EC2
アクション：EC2:DeleteSnapshot
Amazon リソースネーム (ARN)：*

### IAM ロール
ロール名：delete_snapshot_after_ami_deregister
>trusted entity：Lambda
アタッチするポリシー：
AWSLambdaBasicExecutionRole
AmazonEC2DescribeSnapshots
AmazonEC2DeleteSnapshot

## Lambda
### Labmda関数を一から作成
関数名： delete_snapshot_after_ami_deregister
>ロール：既存のロールを選択
既存のロール：delete_snapshot_after_ami_deregister
コードエントリ タイプ：コードをインラインで編集
ランタイム：Python 3.6
ハンドラ 情報：lambda_function.lambda_handler
環境変数：
　キー：AWS_ACCOUNT, 値：AWSアカウントID（12桁の数字）
基本設定：
　タイムアウト：5分

コード

```python
import boto3
import os
from logging import getLogger, INFO
from time import sleep
from botocore.exceptions import ClientError
 
logger = getLogger()
logger.setLevel(INFO)
     
client = boto3.client('ec2')
     
def lambda_handler(event, context):
    imageID = event['detail']['requestParameters']['imageId']
 
    response = client.describe_snapshots(
        OwnerIds = [
            os.environ['AWS_ACCOUNT']
        ],
        Filters = [
            {
                'Name': 'description',
                'Values': [ 'Created by CreateImage(*) for ' + imageID + ' from *' ]
            }
        ]
    )
    for snapshot in response['Snapshots']:
        logger.info(imageID)
        logger.info("delete_snapshot: " + snapshot['SnapshotId'])
        print("delete_snapshot: " + snapshot['SnapshotId'])
        _delete_snapshot(snapshot['SnapshotId'])
         
def _delete_snapshot(snapshotid):
    try:
        return client.delete_snapshot(SnapshotId=snapshotid)
    except ClientError as e:
        logger.exception("Received error: %s", e)
        sleep(2)
```

## CloudWatch イベント
AWS AMI が登録解除されて、DeregisterImage という AWS API Call が呼び出されたときに、Lambda関数 delete_snapshot_after_ami_deregister を実行させる。

### イベントパターンのルールを作成
>サービス名：EC2
イベントタイプ：AWS API Call via CloudTrail
特定のオペレーション：DeregisterImage
ターゲット：Lambda 関数　delete_snapshot_after_ami_deleted


# 2019/03/19 追記
上記の環境を構築する AWS CloudFormation