※ 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