AWS EC2のスナップショットをLambdaで履歴管理する

今まではEC2の中でクーロンが動作してスナップショットをとっていましたが、なーんとなく違和感(自分自身でスナップショットを取るってどうなん?いやちょっと意味合いは違うけど…外側からスナップショット取りたい!)があって、なんか方法ないかなぁと思ったら、ビンゴな記事が!(Qittaさんいつもお世話になっております!!)

Lambdaって?

読み方はラムダ(だと思う。)
詳しいことはここに書いてあるけど、サーバ建てなくてもコードが実行できるちょっと便利なやつ。起動契機もいろいろとある。
お値段は、1ヶ月100万件のリクエスト及び400,000 GB-秒のコンピューでィング時間が無料。
相変わらずよくわからんが、今回の用途ぐらいなら無料枠で行ける。

EC2側の設定

モザイクばかりで申し訳ないが、Nameのところにマウスを合わせると、えんぴつマークが出てくるので、インスタンスに名前をつけてあげる。

「タグ」をクリックして、「タグの追加/編集」をクリックすると、

「タグを作成」をクリックして「Backup-Generation」キーにスナップショットを取る件数を値に入力して「保存」をクリックします。

 

Lambda側で関数を作ってスナップショットを作成する

「今すぐ始める」をクリック

「Python 2.7」を選んで、フィルターに「hello」を入れると「hello-world-python」が出て来るので、そのあたりをクリック。

トリガーについては後ほど設定するので、とりあえず、「次へ」をクリック。

ロールで「カスタムロールを作成」を選択する。

ロール名を適当につけて、「ポリシードキュメント…」をクリックして「編集」ボタンをクリックすると、

確認メッセージが出るので、「OK」ボタンをクリック。

編集する内容は以下を入力する。

{
    "Version": "2017-04-07",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:*:*:*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances",
                "ec2:DescribeSnapshots",
                "ec2:CreateSnapshot",
                "ec2:DeleteSnapshot"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

※あたりまえのことかもだけど、最後に改行があるとエラーでます…

タイムアウトを2秒から10秒に変更して、「次へ」をクリック

確認画面が表示されるので、「関数の作成」をクリック。

コード部分には以下を入力。

import boto3
import collections
import time
from botocore.client import ClientError
ec2 = boto3.client('ec2')

def lambda_handler(event, context):
    descriptions = create_snapshots()
    delete_old_snapshots(descriptions)

def create_snapshots():
    instances = get_instances(['Backup-Generation'])

    descriptions = {}

    for i in instances:
        tags = { t['Key']: t['Value'] for t in i['Tags'] }
        generation = int( tags.get('Backup-Generation', 0) )

        if generation < 1:
            continue

        for b in i['BlockDeviceMappings']:
            if b.get('Ebs') is None:
                continue

            volume_id = b['Ebs']['VolumeId']
            description = volume_id if tags.get('Name') is '' else '%s(%s)' % (volume_id, tags['Name'])
            description = 'Auto Snapshot ' + description

            snapshot = _create_snapshot(volume_id, description)
            print 'create snapshot %s(%s)' % (snapshot['SnapshotId'], description)

            descriptions[description] = generation

    return descriptions

def get_instances(tag_names):
    reservations = ec2.describe_instances(
        Filters=[
            {
                'Name': 'tag-key',
                'Values': tag_names
            }
        ]
    )['Reservations']

    return sum([
        [i for i in r['Instances']]
        for r in reservations
    ], [])

def delete_old_snapshots(descriptions):
    snapshots_descriptions = get_snapshots_descriptions(descriptions.keys())

    for description, snapshots in snapshots_descriptions.items():
        delete_count = len(snapshots) - descriptions[description]

        if delete_count <= 0:
            continue

        snapshots.sort(key=lambda x:x['StartTime'])

        old_snapshots = snapshots[0:delete_count]

        for s in old_snapshots:
            _delete_snapshot(s['SnapshotId'])
            print 'delete snapshot %s(%s)' % (s['SnapshotId'], s['Description']) 

def get_snapshots_descriptions(descriptions):
    snapshots = ec2.describe_snapshots(
        Filters=[
            {
                'Name': 'description',
                'Values': descriptions,
            }
        ]
    )['Snapshots']

    groups = collections.defaultdict(lambda: [])
    { groups[ s['Description'] ].append(s) for s in snapshots }

    return groups

def _create_snapshot(id, description):
    for i in range(1, 3):
        try:
            return ec2.create_snapshot(VolumeId=id,Description=description)
        except ClientError as e:
            print str(e)
        time.sleep(1)
    raise Exception('cannot create snapshot ' + description)

def _delete_snapshot(id):
    for i in range(1, 3):
        try:
            return ec2.delete_snapshot(SnapshotId=id)
        except ClientError as e:
            print str(e)
        time.sleep(1)
    raise Exception('cannot delete snapshot ' + id)

で、「テスト」ボタンをクリック。

入力することは無いので、そのまま「保存してテスト」をクリック。

こんな感じになるので、「ここをクリックし」でもクリックしてみると、

 

こんな感じで、

こんなログが表示される。

ec2のスナップショットを見てみると…

おぉ!!作ってる!!

Lambda側で定期的に実行させる。

「トリガーを追加」をクリック。

空白の点線内をクリックして、「CloudWatch イベント – スケジュール」をクリック。

「ルール名」を適当に入力して「スケジュール式」に「cron(0 22 * * ? *)」を入力して、「送信」ボタンをクリックする。
時刻はUTC時間なので、適当に計算してください…

これで、毎日スナップショットを自動的に取得してくれます。数日後確認したところ、古い履歴は消されて、最新の2件だけスナップショットが取れてました。よかったよかった。

あとは、元の記事にあるようにCloudWatchでエラーを監視したりの設定をしていけばいい感じじゃないでしょうか。

これ、何が便利って、「Backup-Generation」キーがあるものをやってくれるので、インスタンスを追加してもタグ付けだけやってあげれば、自動的にスナップショットとってくれるんですよね。