LoginSignup
33
28

More than 3 years have passed since last update.

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

Last updated at Posted at 2017-12-04

2020/2/25 追記
2020年1月、ようやく EC2 の バックアップ/リストア を支援する AWS Backup EC2 が リリースされた。
私も、これまでこの自作バックアップを使っていたけど、AWS Backup EC2 へ移行した。
AWS Backup EC2 を実運用へ導入したときのあれやこれや

概要

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

参考にさせてもらったページ

EC2のスナップショットを自動的にAWS Lambdaで作成する
AMI削除時にスナップショットも自動で削除するCloudWatch Events

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分

コード

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分

コード

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

33
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
28