AMIを他リージョンへスケジュールコピーするLambda関数

バックアップ目的で他リージョンに AMI をスケジュールコピーするための Lambda 関数の作り方です。

自リージョンで AMI を定期作成する処理は含みません。
今回の Lambda 関数の仕様とは一部合わない部分もありますが、AWS 標準の機能でも定期作成できますし、世の中に定期作成を行うためのスクリプト等はたくさんある、というのが理由です。

# 処理の内容

コピー元のリージョンで特定のタグ名が付いた AMI だけを、コピー先のリージョンへ持っていきます。
最低限のバックアップとするため、コピー先では世代管理せず洗い替えを行います。
なお、コピーする AMI は KMS で作成したキーを使って暗号化するものとします。

 * **[Auto Sync AWS AMIs to another region with Lambda](http://ricktbaker.com/2017/11/27/sync-aws-amis-disaster-recovery-region/)**

にあったスクリプトの一部がコーディングミス？で動かなかったのと、AMI をコピーする条件を変えたかったため、コードを変更しました。

# Lambda 関数の作り方

## 1. ロール作成

順番としては Lambda 関数を作成してから変更しても良いのですが、Lambda 用の標準的なポリシーを含んだロール（CloudWatch Logs へのログ書き出し権限が付いている）に、AWS 管理ポリシー`AmazonEC2FullAccess`を追加します。
![amicopy_01_role.png](https://qiita-image-store.s3.amazonaws.com/0/123899/ae4bb858-015d-88a3-4527-0362681b3243.png)

## 2. Lambda 関数作成

適当な名前を付けて、Python 3.6 の Lambda 関数を作成します。ロールは先ほど作成したものを指定します。
![amicopy_02_lambda_create.png](https://qiita-image-store.s3.amazonaws.com/0/123899/1776d810-fd3d-745b-1476-5920f28c9275.png)

## 3. 実行トリガー作成・設定

CloudWatch Events で定期実行するようトリガーを作成します。AMI が定期作成された後の時刻を指定すると良いでしょう。
![amicopy_03_lambda_trigger.png](https://qiita-image-store.s3.amazonaws.com/0/123899/02df6afe-f2e7-9316-1ea9-c8eedd66e6a0.png)

※スクリーンショットの cron 式で指定している時刻は UTC ですのでご注意を。

## 4. コード入力

```python:lambda_function.lambda_handler
import boto3
import os

def lambda_handler(event, context):
    sourceRegion = os.environ["SOURCE_REGION"]
    destRegion = os.environ["DEST_REGION"]
    sourceTagKey = os.environ["SOURCE_TAG_KEY"]
    destTagKey = os.environ["DEST_TAG_KEY"]
    owner = os.environ["OWNER"]
    encryptKey = os.environ["ENCRYPT_KEY"]
    
    # コピー対象のイメージを抽出
    sourceClient = boto3.client('ec2', sourceRegion)
    sourceImages = sourceClient.describe_images(
        Filters=[{'Name':'tag-key','Values':[sourceTagKey]}],
        Owners=[owner]
    )
    
    # 削除対象のイメージを抽出
    destClient = boto3.client('ec2', destRegion)
    deleteImages = destClient.describe_images(
        Filters=[{'Name':'tag-key','Values':[destTagKey]}],
        Owners=[owner]
    )
    
    # コピー対象イメージがあればコピー実行
    for sourceImage in sourceImages['Images']:
        copyKey = sourceImage['ImageId']
        print("Copying Image '{}' to {}.".format(copyKey, destRegion))
        new_ami = destClient.copy_image(
            DryRun=False,
            SourceRegion=sourceRegion,
            SourceImageId=copyKey,
            Name=sourceImage['Name'],
            Description=sourceImage['Description'],
            Encrypted=True,
            KmsKeyId=encryptKey
            )
        destClient.create_tags(Resources=[new_ami['ImageId']], Tags=[{'Key':destTagKey,'Value':''}])

    # 削除対象イメージがあれば削除実行
    for deleteImage in deleteImages['Images']:
        deleteKey = deleteImage['ImageId']
        print("Deleting Image '{}' from {}.".format(deleteKey, destRegion))
        try:
            destClient.deregister_image(ImageId=deleteKey)
            for snapshot in deleteImage['BlockDeviceMappings']:
                destClient.delete_snapshot(SnapshotId=snapshot['Ebs']['SnapshotId'], DryRun=False)
        except destClient.exceptions.NoSuchEntityException:
            print("Image '{}' not found.".format(deleteKey))
```

## 5. 環境変数指定

以下の通り指定します。
![amicopy_04_lambda_code_02.png](https://qiita-image-store.s3.amazonaws.com/0/123899/9ead0c1b-1e2f-5437-4f87-48e6663b20f3.png)
`DEST_REGION` : コピー先リージョン
`DEST_TAG_KEY` : コピー先の AMI に付与するタグ名
`ENCRYPT_KEY` : 暗号キー（KMS）※コピー先リージョンで作成したもの
`OWNER` : アカウント ID
`SOURCE_REGION` : コピー元リージョン
`SOURCE_TAG_KEY` : コピー元で対象の AMI に付与されているタグ名

タイムアウト（時間）は 1 分程度まで延長しておきます。

## 6. KMS キーのキーユーザーとしてロールを登録

IAM の暗号化キーの画面で、コピー元リージョンで使う KMS キー、コピー先リージョンで使う KMS キー（5. で指定したもの）の両方のキーユーザーとして、1. で作成したロールを追加登録します。

※スクリーンショットは省略。

# 注意

同一リージョンから同時に AMI をコピーする場合、最大 25 個まで、という制限があります。
25個を超える場合は、別のタグ名を付与し、Lambda 関数を対象タグ名別に分割して実行します。

また、

 * 暗号キーとして KMS ではなく AWS のビルトインキーを使いたい場合
 * タグ名以外の項目（タグ名＋タグ値など）で対象を絞りたい場合
 * コピー先でも世代管理したい場合

は、適宜コードを変更してください。

# その他

AWS の某パートナーが提供している自動化ツールが、大阪ローカルリージョンに対応してくれればこれを作る必要もなかったんですけどね…。