AMI をアカウント跨ぎで（他リージョンへ）スケジュールコピーする Lambda 関数

以前、**[AMIを他リージョンへスケジュールコピーするLambda関数](https://qiita.com/hmatsu47/items/98006dfb0509ef5a5642)**という記事を書きましたが、それをアカウントを跨いで実行するためのものです。

# ポイント

 - コピー対象の AMI 全てにコピー先のアカウントに対するアクセス権を付与する
 - コピー元アカウントの KMS キーを利用できるようにコピー先で作成する Lambda 関数のロールに必要なポリシーを付与する
 - コピー先アカウントからはコピー元のタグは不可視なので、別項目（「説明」：description）をキーにしてコピー対象を絞り込む

なお、今回は、

 - AMI のアクセス権付与はコピー元アカウントで
 - AMI コピーはコピー先アカウントで

それぞれ実行するための Lambda 関数を作成します。

※前回同様、コピー元の AMI は KMS カスタマー管理キーで暗号化されているものとします（デフォルトキー＝AWS マネージドキーは不可）。

# 作業内容

### 1. コピー元アカウントで KMS キーにキーユーザーを登録

AMI 暗号化のために使っている KMS カスタマー管理キーに対して、

 - 4. の Lambda 関数で利用するロール（キーユーザーとして）
   - 前回記事「1. ロール作成」と同じ要領で作成。面倒なら 4. の Lambda 関数作成時に合わせて作成して KMS カスタマー管理キーに追加する。↓の 2. も同様。
 - コピー先アカウント（アカウント ID）

を追加します。

### 2. コピー先アカウントで KMS カスタマー管理キーを作成してキーユーザーを登録

カスタマー管理キーを作成し、キーユーザーとして 5. の Lambda 関数で利用するロールを追加します。

### 3. コピー先アカウントでポリシーを登録

コピー元アカウントの KMS カスタマー管理キーを使えるように、コピー先アカウントに以下のポリシーを登録します。

```json:ポリシー登録
{ 
    "Version": "2012-10-17", 
    "Statement": [ 
        { 
            "Sid": "AllowUseOfCMK", 
            "Effect": "Allow", 
            "Action": [ 
                "kms:Encrypt", 
                "kms:Decrypt", 
                "kms:ReEncrypt*", 
                "kms:GenerateDataKey*", 
                "kms:DescribeKey" 
            ], 
            "Resource": "arn:aws:kms:ap-northeast-1:【コピー元アカウントID】:key/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" 
        }, 
        { 
            "Sid": "AllowUseofCMKToCreateEncryptedResources", 
            "Effect": "Allow", 
            "Action": "kms:CreateGrant", 
            "Resource": "arn:aws:kms:ap-northeast-1:【コピー元アカウントID】:key/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", 
            "Condition": { 
                "Bool": { 
                    "kms:GrantIsForAWSResource": true 
                } 
            } 
        } 
    ] 
}
```

※「key/」の後ろにはコピー元 AMI の暗号化に使う KMS カスタマー管理キーの ID を入力します。

登録したら、2. の KMS カスタマー管理キーのキーユーザーとして登録した Lambda 関数のロールにこのポリシーを追加で付与します。

### 4. コピー元アカウントで AMI アクセス権付与のための Lambda 関数を作成

Python 3.7 で以下のコードを登録します。

```python:AMIアクセス権付与
import boto3 
import os 
 
def lambda_handler(event, context): 
    region = os.environ["REGION"] 
    tagKey = os.environ["TAG_KEY"] 
    owner  = os.environ["OWNER"] 
    share  = os.environ["SHARE"] 
     
    # 変更対象のイメージを抽出 
    client = boto3.client('ec2', region) 
    images = client.describe_images( 
        Filters=[{'Name':'tag-key','Values':[tagKey]}], 
        Owners=[owner] 
    ) 
     
    # 変更対象イメージがあれば変更実行 
    for image in images['Images']: 
        modifyKey = image['ImageId'] 
        print("Modifying Image Attribute '{}'.".format(modifyKey)) 
        client.modify_image_attribute( 
            ImageId=modifyKey, 
            LaunchPermission={'Add':[{'UserId':share}]} 
        ) 
        for snapshot in image['BlockDeviceMappings']: 
            client.modify_snapshot_attribute( 
                CreateVolumePermission={'Add':[{'UserId':share}]}, 
                SnapshotId=snapshot['Ebs']['SnapshotId'] 
            ) 
```

環境変数等は以下のように入力します。
![attr-change.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123899/30c86c59-77cd-4dbe-c2fe-b5f4db0e38fc.png)

なお、AMI の抽出対応を 5. と同様に「説明」（description）にしたい場合は 5. を参考にコードを書き替えてください。

テストしてみてうまく動作したら、必要に応じて CloudWatch イベントの cron 式で定時実行をセットします。

### 5. コピー先アカウントで AMI コピーを行う Lambda 関数を作成

Python 3.7 で以下のコードを登録します。

```python:AMIコピー
import boto3 
import os 
 
def lambda_handler(event, context): 
    sourceRegion = os.environ["SOURCE_REGION"] 
    destRegion = os.environ["DEST_REGION"] 
    sourceDescription = os.environ["SOURCE_DESCRIPTION"] 
    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':'description','Values':[sourceDescription]}], 
        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='', 
            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)) 
```

環境変数等は以下のように入力します。
![attr-copy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123899/93f378e2-961e-6bba-452a-473f947d9d0a.png)


※コピー対象の AMI の絞り込みには以下の項目を使います。
![attr-ami.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123899/c4a213cf-bf62-421e-cb93-b5082e2cd177.png)

テストしてみてうまく動作したら、必要に応じて CloudWatch イベントの cron 式で定時実行をセットします（4. の 10 ～ 15 分程度後の時間を指定）。

# その他

以前、一度にコピーできる AMI の数は 25 個だった気がするのですが、最近実行してみたところ 50 個まで同時にコピーできるようになっていました。