AWS Lambda関数でEC2のスナップショットを作成する~対象はEBSボリュームのタグで指定

isdはじめに

AWSでサーバー環境を構築する際、サーバーのバックアップや何らかの設定変更等の定期処理は、Lambda関数を使用して実現することが多いと思います。
そこで、僕がこれまで必要に応じて作成してきたLambda関数をいくつか紹介していきます。

今回はEC2のスナップショットを作成するLambda関数です。

isdLambda関数の仕様・設定

仕様

  • タグのキー ‘Backup-Generation’ を指定したEBSボリュームのスナップショットを作成する。
  • タグのキー ‘Backup-Generation’ の値で指定した世代数のぶんだけ保存し、古い世代のスナップショットは削除する。
  • 作成したスナップショットの「説明」に「Auto Snapshot vol-xxxxxxxx(Auto Snapshot vol-xxxxxxxxxxxxxxxxxx(EC2インスタンス名))」のような文字列を付与する。
  • 保存世代のカウントは、このLambda関数で生成したもののみ対象とし、これ以外の方法で作成したスナップショットは削除しない。
  • Lambda関数のランタイムはPython 3.6

EBSボリュームのタグ

スナップショットを取得したいEBSボリュームに対して、

  • キー: Backup-Generation
  • 値: <保存世代数>

を指定します。


 

Lambda関数

スナップショットを作成する対象となるEC2インスタンスと同じリージョンでLambda関数を作成します。
ランタイムは、Python 3.6を指定します。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# 特定のタグを含むEBSボリュームのスナップショットを作成する。
#	Python 3.6
#
#	(参考)
#	https://qiita.com/HorieH/items/66bb68d12bd8fdbbd076
#
# EBSボリュームが存在するリージョンでLambda関数を作成すること。
#
# 対象とするボリュームのタグで、
# Key: <TAGKEYの文字列>, Value: <保存世代数> 
# を設定すること。
#
TAGKEY = 'Backup-Generation'

import boto3
import collections
import time
from botocore.client import ClientError
import os

client = boto3.client('ec2', os.environ['AWS_REGION'])

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

def create_snapshots():
    volumes = get_volumes([TAGKEY])

    descriptions = {}

    for v in volumes:
        tags = { t['Key']: t['Value'] for t in v['Tags'] }
        generation = int( tags.get(TAGKEY, 0) )

        if generation < 1:
            continue

        volume_id = v['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_volumes(tag_names):
    volumes = client.describe_volumes(
        Filters=[
            {
                'Name': 'tag-key',
                'Values': tag_names
            }
        ]
    )['Volumes']

    return volumes

def delete_old_snapshots(descriptions):
    snapshots_descriptions = get_snapshots_descriptions(list(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 = client.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 client.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 client.delete_snapshot(SnapshotId=id)
        except ClientError as e:
            print(str(e))
            if e.response['Error']['Code'] == 'InvalidSnapshot.InUse':
                return;
        time.sleep(1)
    raise Exception('cannot delete snapshot ' + id)

# EOF

 

実行ロール

Lambda関数に付与するIAMロールのカスタムポリシーは以下のようにします。
CloudWatch Logsのログ書き出しと、EC2ボリューム、スナップショットの情報取得、スナップショットの作成・削除アクションを付与しています。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:GetLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:*:*:*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeVolumes",
                "ec2:DescribeSnapshots",
                "ec2:CreateSnapshot",
                "ec2:DeleteSnapshot"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

 

基本設定

メモリはデフォルトの128MBのままでOKです。
タイムアウトはデフォルトの3秒では短かすぎてエラーすることがあるので、10秒や30秒などに変更します。

定期実行設定

「毎日4時にバックアップ」など、定期的にLambda関数を実行するためには、トリガーとしてCloudWatch Eventを指定して、スケジュールのルールを設定します。

以下は「毎日4時にバックアップ」する設定例です。
「スケジュール式」の時刻はUTCで指定することに注意します。


 

補足

今回のLambda関数は、以下の記事で公開されていた関数をカスタマイズしたものです。
(ありがとうございます!)

・EC2のスナップショットを自動的にAWS Lambdaで作成する – Qiita
https://qiita.com/HorieH/items/66bb68d12bd8fdbbd076

カスタマイズしたポイントは次のとおりです。

  • タグをつけるリソースは、EC2インスタンスではなくEBSボリュームに変更
  • Python 3に対応

タグをEBSボリュームのほうにつけるようにした理由は、EC2インスタンスに複数のEBSボリュームがあり、その一部のみスナップショットを作成したい、という要件があったからです。

具体的には、

  • ROOTボリューム: 10GB
  • 画像(または動画)保存用追加ボリューム: 1TB

というディスク構成のときに、ROOTボリュームのみスナップショットを作成して、1TBのボリュームはスナップショットではなく、実ファイルをS3にバックアップしたい、と。
料金を比較すると、S3は $0.025/GB,月、EBSスナップショットは $0.05/GB,月なので、S3のほうが安いという判断ですね。

料金面でいえば、本当は、最初から画像をEBSのローカルディスクではなくS3に保存して、誤って更新・削除したファイルを復元するためにバージョニングを有効にするのが一番安上がりになります。
複数サーバーからの共有が可能となることや、画像をS3もしくはCloudFrontから配信してサーバーの負荷を軽減するなどのメリットもあります。

ただ、この案件のときはオンプレミスからの移行で、
「アプリケーション側で、画像をローカルディスクではなくS3にアップロードするような修正はできない」
ということで、「EBSに保存、S3にバックアップ」となりました。

isdおわりに

AWS EC2インスタンスのバックアップのため、スナップショットを作成するLambda関数を紹介しました。

残念ながらAWSには、
「管理画面の操作で、定期的にサーバーをバックアップする設定を行う」
という機能は用意されていません。

このため、サーバーのバックアップには、

  • (今回のような)Lambda関数+CloudWatch Eventで設定
  • EC2インスタンスでAWSCLI+cronで設定
  • AWSマネジメントコンソールで手動操作

といった方法を採用することになります。

それでも、EC2インスタンス上でバックアップ処理を実行するのに比べると、Lambdaは可用性が高く、コストがほとんどかからず、設定も簡単で、ずいぶん便利になりました。

EBSボリュームのスナップショットは、元々単価が安いうえ、ディスクサイズではなく実際の使用量分の圧縮・増分で取得できるので、よほどファイルの更新が多くなければ、世代数をたくさん保存しても非常に安価です。
(請求ダッシュボードで、EBSスナップショット分の料金を確認してみるとよいでしょう。)

地味ながら、他のクラウドサービスに比べて優れている機能ですので、
「サーバー内の設定は自動化していて、壊れてもすぐ一から復元できる」
というのでもない限り、スナップショット作成による自動バックアップ設定を行うべきだと思います。

なお、スナップショットではなく
「AMI(イメージ)を作成してバックアップする」
という方法もあります。
この場合、起動中のEC2インスタンスのEBS ROOTデバイスボリュームに対して「AMIの作成」を実行するときに、no-reboot オプションをつけないとEC2インスタンスの再起動が発生することに注意しましょう。

isd(追記)Amazon Data lifecycle Managerについて

※2018.12.7 に追記。

その後、2018年8月に、東京リージョンでAmazon Data lifecycle Manager (Amazon DLM) がリリースされました。
これにより、この記事のように、わざわざLambda関数を使わなくても、AWSの設定でスナップショット作成を自動化できるようになりました。

Amazon DLMの使い方は、以下の記事などを参照してください。

・Amazon DLM で簡単!スナップショット世代管理 – Qiita
https://qiita.com/quickguard/items/3e1778312b93dbe4b2a6

↑この記事には記載がありませんが、その後、EBSボリュームのNameタグをそのままスナップショットにコピーする「Copy Tags from Volume」というオプションが追加されたのはうれしいです。

Lambda + CloudWatch Eventsで設定するのに比べると、以下の点で少し劣りますが、通常は問題ないと思います。

  • スナップショットの取得頻度が「12時間おき」か「24時間おき」のどちらかしか選択できない。
  • スナップショットを作成する時刻は厳密ではなく、指定した開始時刻から1時間以内

ということで、最近は僕もAmazon DLMを利用して、サーバーのバックアップ設定を行うことにしています。

(関連記事)
・AWS Lambda関数でRDSインスタンスのMulti-AZ有効無効を切り替える
https://inaba-serverdesign.jp/blog/20180509/aws_rds_multiaz_switch_lambda.html
 

Follow me!