EC2のスナップショットを自動的にAWS Lambdaで作成する

![Compute_Lambda.png](https://qiita-image-store.s3.amazonaws.com/0/69627/edc6afc8-201d-cfbb-cfe6-ef9d35df63be.png) AWS Lambda?
----
- サーバレスでプログラムを実行できるようなもの
- 1 か月 1,000,000 リクエストは無料
    - 今回のような１日１回動かす程度なら無料でずっと使えます
- 詳しくは → https://aws.amazon.com/jp/lambda/faqs/


やりたいこと
-----
- Lambdaのスケジュールイベントを使って自動的にEBSのスナップショットを作成する
    - 今回使用する言語はPython
- スナップショットの対象は、EC2インスタンスのタグに「Backup-Generation」が付いているものとする
    - Backup-Generationの値には、0以上の数字を指定する
        - スナップショットの保存件数は、Backup-Generationの値とする
            - 0のときはスナップショットを作成しない
        - スナップショットの保存件数を超えた場合は、作成日付の古いものから削除する
- 作成したスナップショットのDescriptionには、「EBSボリュームID」と「EC2インスタンス名」をつける
    - EC2インスタンス名がない場合は、「EBSボリュームID」のみをつける
- 処理が失敗した場合は、CloudWatchの監視によりメール（SNS）を送る

EC2インスタンスに名前とタグをつける
-----------------

### 名前をつける

![名前をつける](https://qiita-image-store.s3.amazonaws.com/0/69627/313727ed-d653-8059-c0b0-c2ab6ad52c72.png)

1. EC2のページのサイドメニューから「Instances」をクリック
2. カーソルをNameの列に合わせると鉛筆アイコンが表示されるのでクリック
3. 名前を入力

### タグをつける

![タグを追加](https://qiita-image-store.s3.amazonaws.com/0/69627/a584af20-59a8-55f4-ac10-3ebcba69420b.png)

1. 対象のインスタンスにチェックをつける
2. 「Tags」ボタンをクリック
3. 「Add/Edit Tags」ボタンをクリック

![タグを追加](https://qiita-image-store.s3.amazonaws.com/0/69627/154e75d2-3779-1c90-2f56-769f603ab610.png)

1. 「Create Tag」ボタンをクリック
2. Keyに「Backup-Generation」、Valueに保存件数を入力
3. 「Save」ボタンをクリック

Lambdaの作成
----------
### Select blueprint

![Select blueprint](https://qiita-image-store.s3.amazonaws.com/0/69627/66033598-8929-46b4-bbf4-f548374c3400.png)

1. Lambdaのページの作成ボタンをクリック
2. 「Python2.7」を選択
3. 「hello-world-python」をクリック（動作確認後にスケジュールイベントを設定したいため）
    - ※「lambda-canary」はいきなりスケジュール設定画面から始まる

### Configure function

![Configure function](https://qiita-image-store.s3.amazonaws.com/0/69627/116cfcf7-8fae-4690-6909-056a3e088e86.png)

1. Nameを入力
2. Descriptionは空でもOK

### Lambda function code

![Lambda function code](https://qiita-image-store.s3.amazonaws.com/0/69627/92fbb5f1-5a9a-1faa-f513-eb9a8952e6cb.png)

1. 下記のコードを貼り付ける

```py:python 
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)
```

### Lambda function handler and role

![Lambda function handler and role](https://qiita-image-store.s3.amazonaws.com/0/69627/138683b6-d3fa-c4f9-25c2-ea137d579935.png)

1. 「Role」から「Basic execution role」を選択

### IAM Roleの作成

![IAM Roleの作成](https://qiita-image-store.s3.amazonaws.com/0/69627/c42bc0bf-28b5-8486-5e48-48a36135abc4.png)

1. 「Role Name」を入力
2. 「View Policy Document」をクリックしコードを表示させる
3. 「Edit」をクリック

![confirm](https://qiita-image-store.s3.amazonaws.com/0/69627/8a248aaa-c523-e546-31bd-70bdde11de5f.png)

4. 「OK」ボタンをクリック
5. 下記のコードを貼り付け
6. 「Allow」ボタンをクリック

```json:iam.json
{
    "Version": "2012-10-17",
    "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": [
                "*"
            ]
        }
    ]
}
```


### Advanced settings

![Advanced settings](https://qiita-image-store.s3.amazonaws.com/0/69627/ebfe8c35-5fde-5e29-591f-fe50912c5958.png)

1. Timeoutを「10 sec」に変更（※動作確認の「Duration」の値を見て適宜変更）
2. 「Next」ボタンをクリック
3. 「Create function」ボタンをクリック

Lambdaの動作確認
-----------
![Test](https://qiita-image-store.s3.amazonaws.com/0/69627/e2db4dc7-ba11-3ecf-48cd-b58f64f6d817.png)

1. 左上の「Test」ボタンをクリック
2. Inputデータは使わないのでそのまま「Save and test」をクリック

![result](https://qiita-image-store.s3.amazonaws.com/0/69627/dc6150be-b70f-21d9-30dd-35d84e550438.png)

2. 動作結果はページ最下部に表示される
    - Summary
        - Duration … 実行時間
        - Billed duration … 課金対象時間
        - Max memory used … メモリ使用量

![CloudWatch](https://qiita-image-store.s3.amazonaws.com/0/69627/7fed4ee0-2273-f2e3-7b7b-ffc69e4ada0b.png)


3. Log outputの「Click here」をクリックするとCloudWatchログの画面が表示される

Lambdaのスケジュールイベントの作成
-----------------------------
### スケジュールイベントの追加

![スケジュールイベントの追加](https://qiita-image-store.s3.amazonaws.com/0/69627/e7a67410-1c64-286a-0652-3cd9d00788c8.png)

1. 「Event sources」をクリック
2. 「Add event source」をクリック

### Add event source

![Add event source](https://qiita-image-store.s3.amazonaws.com/0/69627/3f55baf7-dbda-db98-6b81-85018a2dd5fe.png)

1. Event source typeから「Scheduled Event」を選択
2. Nameを入力
3. Descriptionは空でもOK
4. schedule expressionを入力　※時間はUTC
    - 参考サイト：[書き方](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/getting-started-scheduled-events.html)、[時差計算](http://www.jisakeisan.info/)
    - 例＞毎日 03:10 (日本時間の深夜) に起動させたい場合
        - cron(10 18 * * ? *)
5. 「Submit」ボタンをクリック

処理の失敗をCloudWatchで監視してSNSでメールを送る
-----------------------------
### Create Alarm

![Create Alarm](https://qiita-image-store.s3.amazonaws.com/0/69627/210b7da7-d5ea-a5c5-0806-ea05045c0273.png)

1. CloudWatchのページのサイドメニューから「Alarms」をクリック
2. 「Create Alarm」ボタンをクリック

### Select Metric-1

![Select Metric-1](https://qiita-image-store.s3.amazonaws.com/0/69627/3d7f53aa-fc30-8e8c-b84e-6caeeabff96f.png)

1. Lambda Metricsの下の「By Function Name」をクリック

### Select Metric-2

![Select Metric-2](https://qiita-image-store.s3.amazonaws.com/0/69627/85800e94-9b4b-3b6d-ca52-17493ad22359.png)

1. Metric Nameが「Errors」にチェック
2. 「Next」ボタンをクリック

### Define Alarm

![Define Alarm](https://qiita-image-store.s3.amazonaws.com/0/69627/ce0063a2-c381-edf6-0769-26032ba5128d.png)

#### Alarm Threshold
1. Nameを入力
2. Wheneverのisを「>」を選択

#### Alarm Preview
1. Periodを「1 Day」を選択（※Lambdaのスケジュールイベントの設定に合わせて適宜変更）
2. Statisticを「Sum」を選択

#### Actions
1. Send notification toの「New list」をクリック（※既存の通知用のSNSがある場合はそれを選択）
2. topic nameを入力
3. Email listに通知するメールアドレスを入力
4. 「Create Alarm」ボタンをクリック
5. 承認メールが送信されるのでメール本文の「Confirm subscription」をクリック

参考サイト
---------
- [AWS Lambda のスケジュールイベントで定期的に RDS の Snapshot を作成する](http://tkuchiki.hatenablog.com/entry/2015/11/06/124113)
- [Scheduling EBS Snapshots](https://serverlesscode.com/post/lambda-schedule-ebs-snapshot-backups/)
- [Bboto3 Docs EC2](http://boto3.readthedocs.org/en/latest/reference/services/ec2.html)