【AWS 】SSM  Documentを使ったAMIのクロスアカウントコピー方法を解説【実践例付き】

# 【AWS 】SSM Documentを使ったAMIのクロスアカウントコピー方法を解説【実践例付き】
## はじめに
AWSでアカウント間にてAMIをコピーしたい場面は少なくありません。
しかし、手作業で行うとエラーのリスクが高まり、運用負荷も増えます。
この記事では、SSM Documentを活用してAMIを効率的にクロスアカウントコピーする方法を、実践例とともに解説します。

この記事を読むと次のことが分かります：
- SSM Documentを使ったAMIコピーの基本
- クロスアカウントに必要なIAMロールやポリシーの設定方法
- CloudFormationを用いた実装例


## SSM Documentとは？
SSM Document (Systems Manager Document) は、AWS Systems Managerの機能の一部で、AWSリソースの管理や運用タスクを自動化するために使用されるJSONまたはYAML形式のドキュメントです。これにより、タスクの実行内容や設定を定義して、リソースに対する操作を簡単かつ標準化できます。
## AWS Backupと比較したSSM Documentのメリット
AMIを自動で取得する方法として、AWS Backupを利用することは多いと思います。
ここでは、AWS Backupと比較した時、SSM Documentを使うメリットを以下にまとめました。

- 柔軟な実行タイミング
    - インスタンス上で直接コマンドを使用してドキュメントを実行可能
    - バッチファイルなどを使用して、特定の処理が完了した後に自動でAMI取得を実行するなど、AWS Backupでは難しい柔軟なタイミング制御が可能
- 細かなカスタマイズ
    - ドキュメント内で複数のAPI操作を組み合わせることで、バックアップ時にタグ付けや追加処理を自動化
    - 条件分岐やパラメータの設定により、実行内容を環境や状況に応じてカスタマイズ可能
- オンデマンドの実行
    - AWS Management Console、AWS CLI、またはAWS SDKを使用して、必要なタイミングで即座に実行
    - AWS Backupのようにスケジュールに依存しないオンデマンド実行が可能
- リアルタイムのフィードバック
    - 実行ステータスやログがリアルタイムで確認できるため、問題発生時のトラブルシューティングが迅速
- 複数アカウントやリージョン対応の簡便さ
    - クロスアカウントやクロスリージョンでのAMIコピーや処理が容易に設定可能

## 実践
### 前提
それでは早速実践に移ります。
今回は、例としてコピー元アカウントの東京リージョンからコピー先アカウントの大阪リージョンにAMIをバックアップ
するSSM Documentを用意します。


ターゲット準備として、コピー元のアカウントで以下を用意しておきます。
- EC2
    - OS
        - Windows2018(Windows系なら指定なし)
    - EBS
        - 暗号化なし
    - IAM
        - このあとの事前準備で作成した物をアタッチ
    - SSM Agent
        - 無ければインストール
    - AWS CLI
        - コマンド実行時に必要

### 事前準備
#### 1.コピー先アカウントでクロスアカウント用のIAMロール作成
まずクロスアカウントのために、AMIコピー先のアカウントでIAMロールを用意します。
コンソール画面のIAMロールに移動し、ロール→ロール作成
信頼されたエンティティタイプで「AWS アカウント」を選択
AWS アカウントは、「別の AWS アカウント」を選択し、コピー元のアカウントIDを入力
![スクリーンショット_2024-12-08_19_57_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957407/a08e689a-8877-5b2c-7ac7-9e4fea40b2df.png)


許可ポリシー は、「AmazonEC2FullAccess」「AmazonSSMFullAccess」を付与し、
「SSMDocumentAssumeRole」という名前でIAMロールを作成。

作成後は、信頼関係の「信頼されたエンティティ」を以下に編集

#### 信頼されたエンティティ
```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::[コピー元アカウントID]:root"
            },
            "Action": "sts:AssumeRole"
        },
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ssm.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```

#### 2.コピー元アカウントのEC2用のIAMポリシーとロール作成・アタッチ
以下のIAMポリシーを作成します。
コンソール画面のIAMロールに移動し、ポリシー→ポリシー作成
##### iam_PassRole_ForAMIBackup

```iam_PassRole_ForAMIBackup
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "iam:PassRole",
                "ssm:StartAutomationExecution"
            ],
            "Resource": [
                "arn:aws:ssm:*:[コピー元アカウントID]:automation-definition/*:*",
                "arn:aws:iam::[コピー元アカウントID]:role/*"
            ]
        }
    ]
}
```

コンソール画面のIAMロールに移動し、ロール→ロール作成
信頼されたエンティティタイプで「AWS のサービス」を選択
ユースケースで「EC2」を選択
許可ポリシーで「AmazonSSMFullAccess」と作成した「iam_PassRole_ForAMIBackup」を付与し、任意の名前でIAMロールを作成。

![スクリーンショット 2024-12-11 22.26.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957407/d333004b-03af-feb6-8fcd-edaeac4da2b4.png)

![スクリーンショット 2024-12-11 22.28.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957407/383c2fe6-7d57-daf5-2462-6f8c338cc08b.png)

上記IAMロールが作成できたら、今回使用するEC2にアタッチしてください。


#### 3. 対象のインスタンスにタグ付け
コピー先アカウントはタグで判別するように作成しているため、以下のコピーしたいEC2に以下のタグを付与します。

| Key | Value |備考|
|-----------|------------|------------|
| CopyAccountId  | 111111111111|コピー先のアカウントIDを記載してください|

### CloudFormationでSSM Documentのデプロイを実行
以下のCloudFormationテンプレートを使って、SSM Documentを簡単にデプロイできます
※CopyAMICrossAccountPolicyの作成の部分のアカウントIDは置き換えてください。
```cross_account_ami_backup.yaml
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  IAMPolicyCopyAMICrossAccountPolicy: 
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: "CopyAMICrossAccountPolicy"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - "sts:AssumeRole"
            # XXXXXXXXXXXはコピー先のアカウントIDを入れてください。
            Resource:
              - "arn:aws:iam::XXXXXXXXXXX:role/SSMDocumentAssumeRole"
  MySSMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ssm.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/CloudWatchFullAccess
        - arn:aws:iam::aws:policy/AmazonSSMFullAccess
        - arn:aws:iam::aws:policy/AmazonEC2FullAccess
        - arn:aws:iam::aws:policy/AmazonS3FullAccess
        - !Ref IAMPolicyCopyAMICrossAccountPolicy
      Path: "/"
      RoleName: AMIBackupRoleForSSM

  SsmDocument:
    Type: AWS::SSM::Document
    Properties:
      DocumentType: Automation
      Name: AmiBackup
      Content:
        schemaVersion: "0.3"
        description: "AMI Backup and Lotate"
        assumeRole: !Sub arn:aws:iam::{{global:ACCOUNT_ID}}:role/${MySSMRole}
        parameters:
          InstanceId:
            type: String
            description: (Mandatory) Id of ec2 instance.
        mainSteps:
#タグ取得
          - name: getInstanceTag
            action: aws:executeAwsApi
            inputs:
              Service: ec2
              Api: DescribeTags
              Filters:
                - Name: resource-id
                  Values: 
                    - "{{InstanceId}}"
                - Name: key
                  Values:
                    - "CopyAccountId"
                    - "Name"
            outputs:
            - Name: CopyAccountId
              Selector: $.Tags[0].Value
            - Name: InstanceName
              Selector: $.Tags[1].Value
#コピー先指定
          - name: GetCopyTargetInfo
            action: aws:executeScript
            onFailure: Abort
            maxAttempts: 3
            inputs:
              Runtime: PowerShell Core 6.0
              InputPayload:
                CopyAccountId: "{{getInstanceTag.CopyAccountId}}"
                region: "{{global:REGION}}"
              Script: |
                Install-Module AWS.Tools.EC2 -Force
                Import-Module AWS.Tools.EC2

                $json = $env:InputPayload | ConvertFrom-Json
                $copyAccountId = $json.CopyAccountId
                $fromRegion = $json.region
                $toRegion =  "ap-northeast-3"

                return @{ CopyAccountId= $copyAccountId; FromRegion = $fromRegion; ToRegion = $toRegion }
            outputs:
              - Name: CopyAccountId
                Selector: $.Payload.CopyAccountId
                Type: String
              - Name: FromRegion
                Selector: $.Payload.FromRegion
                Type: String
              - Name: ToRegion
                Selector: $.Payload.ToRegion
                Type: String
#AMI作成(コピー元アカウント)
          - name: createImage 
            action: aws:createImage
            inputs:
              InstanceId: "{{InstanceId}}"
              ImageName: "{{getInstanceTag.InstanceName}}-{{global:DATE_TIME}}"
              NoReboot: true
#AMIのステータスチェック(コピー元アカウント)
          - name: checkAmiStatus
            action: aws:waitForAwsResourceProperty
            timeoutSeconds: 1800
            onFailure: Abort
            inputs:
              Service: ec2
              Api: DescribeImages
              ImageIds:
              - "{{createImage.ImageId}}"
              PropertySelector: "$.Images[0].State"
              DesiredValues:
              - available
#AMIにタグを付与(コピー元アカウント)
          - name: addTag
            action: aws:createTags
            maxAttempts: 3
            onFailure: Abort
            inputs:
              ResourceType: EC2
              ResourceIds:
              - "{{createImage.ImageId}}"
              Tags:
              - Key: Name
                Value: "{{getInstanceTag.InstanceName}}"
              - Key: BackupIdentifier
                Value: "CreatedBySSMAutomation"
              - Key: DRTarget
                Value: "True"
              - Key: CopyAccountId
                Value: "{{getInstanceTag.CopyAccountId}}"
# #AMIからEBSスナップショットIDを取得
          - name: GetSnapshotIds
            action: aws:executeScript
            onFailure: Abort
            maxAttempts: 3
            inputs:
              Runtime: PowerShell Core 6.0
              InputPayload:
                ImageId: "{{createImage.ImageId}}"
              Script: |
                Install-Module AWS.Tools.EC2 -Force
                Import-Module AWS.Tools.EC2

                $json = $env:InputPayload | ConvertFrom-Json
                $image = Get-EC2Image -ImageId $json.ImageId
                $snapshots = @()
                foreach ($device in $image.BlockDeviceMappings) {
                  if ($device.Ebs.SnapshotId -ne $null) {
                    $snapshots += $device.Ebs.SnapshotId
                  }
                }
                return @{ "SnapshotIds" = $snapshots }
            outputs:
              - Name: SnapshotIds
                Selector: $.Payload.SnapshotIds
                Type: StringList
# EBSスナップショットに共有許可を追加
          - name: shareSnapshots
            action: aws:executeScript
            onFailure: Abort
            maxAttempts: 3
            inputs:
              Runtime: PowerShell Core 6.0
              InputPayload:
                SnapshotIds: "{{GetSnapshotIds.SnapshotIds}}"
                CopyAccountId: "{{GetCopyTargetInfo.CopyAccountId}}"
              Script: |
                Install-Module AWS.Tools.EC2 -Force
                Import-Module AWS.Tools.EC2

                $json = $env:InputPayload | ConvertFrom-Json
                foreach ($snapshotId in $json.SnapshotIds) {
                  Edit-EC2SnapshotAttribute -SnapshotId $snapshotId -Attribute createVolumePermission -OperationType add -UserIds $json.CopyAccountId
                }
#コピー先アカウント大阪リージョンへのバックアップ
          - name: copyAmi
            action: aws:executeScript
            onFailure: Abort
            maxAttempts: 3
            inputs:
              Runtime: PowerShell Core 6.0
              InputPayload:
                InstanceName: "{{getInstanceTag.InstanceName}}"
                ImageId: "{{createImage.ImageId}}"
                CopyAccountId: "{{GetCopyTargetInfo.CopyAccountId}}"
                FromRegion: "{{GetCopyTargetInfo.FromRegion}}"
                ToRegion: "{{GetCopyTargetInfo.ToRegion}}"
              Script: |
                Install-Module AWS.Tools.EC2 -Force
                Install-Module AWS.Tools.SimpleSystemsManagement -Force
                Install-Module AWS.Tools.Common -Force
                Install-Module AWS.Tools.SecurityToken -Force

                Import-Module AWS.Tools.EC2
                Import-Module AWS.Tools.SimpleSystemsManagement
                Import-Module AWS.Tools.Common
                Import-Module AWS.Tools.SimpleSystemsManagement

                $json = $env:InputPayload | ConvertFrom-Json

                # コピー先アカウントのIAMロールを引き受け
                $roleArn = "arn:aws:iam::" + $json.CopyAccountId + ":role/SSMDocumentAssumeRole"
                $assumedRole = (Use-STSRole -RoleArn $roleArn -RoleSessionName "CheckAmiStatusSession").Credentials
                Set-AWSCredentials -AccessKey $assumedRole.AccessKeyId -SecretKey $assumedRole.SecretAccessKey -SessionToken $assumedRole.SessionToken

                # 大阪リージョンへAMIをコピー
                $amiId = Copy-EC2Image -SourceRegion $json.FromRegion -SourceImageId $json.ImageId -Region $json.ToRegion -Name "$($json.InstanceName)"
                return @{"OsakaAmiId"=$amiId}
            outputs:
            - Name: OsakaAmiId
              Selector: $.Payload.OsakaAmiId
              Type: String
#AMIのステータスチェック(コピー先アカウント)
          - name: checkCopyAccountAmiStatus
            action: aws:executeScript
            onFailure: Abort
            timeoutSeconds: 1800
            maxAttempts: 3
            inputs:
              Runtime: PowerShell Core 6.0
              InputPayload:
                OsakaAmiId: "{{copyAmi.OsakaAmiId}}"
                InstanceName: "{{getInstanceTag.InstanceName}}"
                CopyAccountId: "{{GetCopyTargetInfo.CopyAccountId}}"
                ToRegion: "{{GetCopyTargetInfo.ToRegion}}"
              Script: |
                Install-Module AWS.Tools.EC2 -Force
                Install-Module AWS.Tools.SimpleSystemsManagement -Force
                Install-Module AWS.Tools.Common -Force
                Install-Module AWS.Tools.SecurityToken -Force

                Import-Module AWS.Tools.EC2
                Import-Module AWS.Tools.SimpleSystemsManagement
                Import-Module AWS.Tools.Common
                Import-Module AWS.Tools.SimpleSystemsManagement

                $json = $env:InputPayload | ConvertFrom-Json

                # コピー先アカウントのIAMロールを引き受け
                $roleArn = "arn:aws:iam::" + $json.CopyAccountId + ":role/SSMDocumentAssumeRole"
                $assumedRole = (Use-STSRole -RoleArn $roleArn -RoleSessionName "CheckAmiStatusSession").Credentials
                Set-AWSCredentials -AccessKey $assumedRole.AccessKeyId -SecretKey $assumedRole.SecretAccessKey -SessionToken $assumedRole.SessionToken

                # 大阪リージョンのAMIステータスをチェック
                $cnt = 0
                do {
                  $OsakaAmiStatus = Get-EC2Image -Region $json.ToRegion -ImageId $json.OsakaAmiId
                  Write-Host @($cnt,$OsakaAmiStatus.State)
                  $cnt++
                  Start-Sleep -Seconds 60
                }while($OsakaAmiStatus.State -ne "available")
#AMIにタグを付与(コピー先アカウント)
          - name: addTagCopyAccount
            action: aws:executeScript
            onFailure: Abort
            maxAttempts: 3
            inputs:
              Runtime: PowerShell Core 6.0
              InputPayload:
                OsakaAmiId: "{{copyAmi.OsakaAmiId}}"
                InstanceName: "{{getInstanceTag.InstanceName}}"
                CopyAccountId: "{{GetCopyTargetInfo.CopyAccountId}}"
                ToRegion: "{{GetCopyTargetInfo.ToRegion}}"
              Script: |
                Install-Module AWS.Tools.EC2 -Force
                Install-Module AWS.Tools.SimpleSystemsManagement -Force
                Install-Module AWS.Tools.Common -Force
                Install-Module AWS.Tools.SecurityToken -Force

                Import-Module AWS.Tools.EC2
                Import-Module AWS.Tools.SimpleSystemsManagement
                Import-Module AWS.Tools.Common
                Import-Module AWS.Tools.SimpleSystemsManagement

                $json = $env:InputPayload | ConvertFrom-Json

                # コピー先アカウントのIAMロールを引き受け
                $roleArn = "arn:aws:iam::" + $json.CopyAccountId + ":role/SSMDocumentAssumeRole"
                $assumedRole = (Use-STSRole -RoleArn $roleArn -RoleSessionName "AddTagOsakaSession").Credentials
                Set-AWSCredentials -AccessKey $assumedRole.AccessKeyId -SecretKey $assumedRole.SecretAccessKey -SessionToken $assumedRole.SessionToken

                # 大阪リージョンのAMIにタグを付与
                $Tags = @( @{key="Name";value=$json.InstanceName},@{key="BackupIdentifier";value="CreatedBySSMAutomation"},@{key="CopyAccountId";value=$json.CopyAccountId} )
                New-EC2Tag -Resource $json.OsakaAmiId -Tag $Tags -Region $json.ToRegion

```

### 各ステップの解説
#### ステップ 1: getInstanceTag (インスタンスのタグを取得)
- action: aws:executeAwsApi
    - EC2 APIを実行
- 目的: 
    - InstanceIdで指定されたEC2インスタンスのタグを取得
    - 以下を抽出
        - CopyAccountId: コピー先アカウントID
        - InstanceName: インスタンス名
#### ステップ 2: GetCopyTargetInfo (コピー先情報の設定)
- action: aws:executeScript
    - PowerShellスクリプトでタグ情報取得
- 目的:
    - CopyAccountId: コピー先アカウントIDを保持
    - FromRegion: 現在のリージョンを保持
    - ToRegion: 大阪リージョン(ap-northeast-3)を設定
#### ステップ 3: createImage (AMI作成)
- action: aws:createImage
    - EC2インスタンスのAMIを作成
- 目的: 
    - 指定されたインスタンスの状態を保持するAMIを作成
#### ステップ 4: checkAmiStatus (AMI作成のステータス確認)
- action: aws:waitForAwsResourceProperty
    - AMIの作成ステータスをチェック
- 目的: 
    - AMIが「available」になるまで待機
#### ステップ 5: addTag (AMIにタグ付与)
- action: aws:createTags
    -  AMIにタグを付与
-  目的: 
    -  作成したAMIにタグを付加する
        -  Name: インスタンス名
        -  BackupIdentifier: バックアップ識別子
        -  CopyAccountId: コピー先アカウントのID
#### ステップ 6: GetSnapshotIds (スナップショットIDを取得)
- action: aws:executeScript
    -  PowerShellスクリプトで、スナップショットIDを取得
-  目的:
    -  AMIに関連付けられたEBSスナップショットIDを収集

#### ステップ 7: shareSnapshots (スナップショットの共有設定)
- action: aws:executeScript
    - PowerShellスクリプトで、スナップショットの共有設定
- 目的:
    - スナップショットの「ボリューム作成権限」をコピー先アカウントに付与
#### ステップ 8: copyAmi (AMIを大阪リージョンにコピー)
- action: aws:executeScript
    - PowerShellスクリプトで、AMIを大阪リージョンにコピー
- 目的:
    - コピー先アカウントでIAMロールを引き受け
    - 元のAMIを大阪リージョンにコピー

#### ステップ 9: checkCopyAccountAmiStatus (コピー後のAMIステータス確認)
- action: aws:executeScript
    - PowerShellスクリプトで、コピー後のAMIステータス確認
- 目的:
    - コピー先アカウントで大阪リージョンのAMIステータスを確認
    - availableになるまでループ
#### ステップ 10: addTagCopyAccount (コピー後のAMIにタグ付与)
- action: aws:executeScript
    - PowerShellスクリプトを実行
- 目的:
    - コピー後のAMIに情報を付加するタグを設定

### 実践方法
実際に動作を確認するために今回は、以下のバッチファイルをEC2上に配置します。
インスタンスIDは自身の環境に置き換えてください。
```backup.bat
aws ssm start-automation-execution ^
    --document-name "AmiBackup" ^
    --parameters "InstanceId=i-XXXXXXXXXXXXXXX"
```
配置したバッチファイルを実行するとドキュメントの実行が開始されます。



実行結果の確認は、マネジメントコンソール上のSystems Managerの「自動化」ページで以下のように結果を確認することができます。
Execution IDをクリックすることで更に詳細まで確認できます。
![スクリーンショット_2024-12-11_22_29_28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957407/76801d88-6248-c69a-9acb-889563b54530.png)




ステータスが「Success」になればコピー完了で、AMIがコピーされています。
![スクリーンショット_2024-12-11_23_11_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2957407/3a78c750-aad1-2629-30d3-054bee11589b.png)

# 最後に
今回は、コピー元アカウントの東京リージョンからコピー先アカウントの大阪リージョンにAMIをコピーしました。
実践で作成したCfnを一部改修することで、宛先のリージョンを変更したり、タグから宛先のリージョンを指定したりすることも可能です。

また、今回確認はしていないのですが、Linux環境でも同様のシェルスクリプトを用意すれば動作すると思うので確認できれば情報更新します。

以上参考になれば幸いです。

##### 参考資料
- [AWS公式ドキュメント: AWS Systems Manager Automation](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-automation.html)