【決定版】KMS暗号化付きS3クロスアカウントレプリケーションをCloudFormationで完全自動化する

S3アイキャッチ

AWSでマルチアカウント運用をしていると必ず遭遇するのが、「AWS1(送信元アカウント)のS3バケットに入ってきたファイルを、AWS2(送信先アカウント)へ自動的にレプリケーション(コピー)したい」という要件です。

これが「デフォルト暗号化(SSE-S3)」なら簡単なのですが、「KMS(CMK)によるカスタマー管理鍵での暗号化」が絡んだ瞬間、難易度が跳ね上がります。 権限周りが複雑になり、レプリケーションステータスが FAILED になって進まない……という罠が随所に仕掛けられているからです。

この記事では、インフラをすべてCloudFormation(Cfn)でコード管理(IaC)しながら、暗号化の壁を越えるクロスアカウントレプリケーションを1発で成功させる手順を、動くコード付きで完全解説します!

全体構成と前提条件

今回のアーキテクチャのポイントは以下の4つです。

  1. 双方向のバージョニング有効化(レプリケーションの必須条件)
  2. 送信元(AWS1)のレプリケーション用IAMロール作成
  3. 送信先(AWS2)のKMSキーポリシー(Conditionによるロール縛り)
  4. 送信先(AWS2)のS3バケットポリシー(他アカウントからの受け入れ許可)

これらを、CloudShellから環境変数(.env)を使ってスマートにデプロイしていきます。

【フェーズ1】AWS2(送信先)での受け入れ準備

まずは、データを受け取る側の AWS2 から作業を開始します。

1. KMSキーの作成(AWS2)

クロスアカウントで最大の障壁となるのが、KMSのキーポリシーです。まだ実在しないAWS1のロール名をキーポリシーに直書きすると、CloudFormationが「そのIAMロールは存在しない」とエラーを吐いて止まってしまいます。

そこで、「アカウント全体を信頼しつつ、Condition(条件句)で特定のロール名に縛る」というテクニックを使います。

まずは環境変数ファイル kms-aws2.env を作成します。
作業環境はAWS CloudShell を利用します。

cat << 'EOF' > kms-aws2.env
AccountName=aws2
Aws1AccountId=123456789012              # あなたのAWS1の12桁アカウントIDに書き換えてください
Aws1ReplicationRoleName=aws1-s3-replication-role # 将来名前を変えたくなったらここを修正
EOF

続いて、KMS用のテンプレート kms.yaml を作成します。

AWSTemplateFormatVersion: '2010-09-09'
Description: 'KMS Key and Dedicated KMS IAM Role (Dynamic Config)'

Parameters:
  AccountName:
    Type: String
    Description: 'Prefix for resource names (e.g., aws1)'
  Aws1AccountId:
    Type: String
    Description: 'AWS1 12-digit Account ID'
  Aws1ReplicationRoleName:
    Type: String
    Description: 'IAM Role Name for replication in AWS1'

Resources:
  # ------------------------------------------------------------
  # 1. KMSキー本体とエイリアスの作成
  # ------------------------------------------------------------
  S3EncryptionKey:
    Type: AWS::KMS::Key
    Properties:
      Description: !Sub "${AccountName}-s3-key"
      Enabled: true
      KeyPolicy:
        Version: '2012-10-17'
        Id: !Sub "${AccountName}-s3-key-policy"
        Statement:
          # ルートアカウントに権限を委譲
          - Sid: Enable IAM User Permissions
            Effect: Allow
            Principal:
              AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
            Action: "kms:*"
            Resource: "*"
          # ------------------------------------------------------------
          # ★Conditionを使って、ロールがまだなくても通るようにする
          # ------------------------------------------------------------
          - Sid: Allow AWS1 Replication Role to Encrypt
            Effect: Allow
            Principal:
              # 一旦「AWS1アカウント全体」を信頼(これなら実在するのでCfnが通る)
              AWS: !Sub "arn:aws:iam::${Aws1AccountId}:root"
            Action:
              - 'kms:Encrypt'
              - 'kms:GenerateDataKey'
            Resource: "*"
            # ★ここで「でも、実際に鍵を使えるのはこのロール名だけ!」と縛りをかける
            Condition:
              ArnEquals:
                aws:PrincipalArn: !Sub "arn:aws:iam::${Aws1AccountId}:role/${Aws1ReplicationRoleName}"

  S3EncryptionKeyAlias:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: !Sub "alias/${AccountName}-s3-key"
      TargetKeyId: !Ref S3EncryptionKey

  # ------------------------------------------------------------
  # 2. KMS専用のIAMロール作成
  # ------------------------------------------------------------
  KMSDedicatedRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${AccountName}-KMSDedicatedRole"
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: !Sub "${AccountName}-KMSAccessPolicy"
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - kms:Encrypt
                  - kms:Decrypt
                  - kms:GenerateDataKey
                  - kms:DescribeKey
                Resource:
                  - !GetAtt S3EncryptionKey.Arn

環境変数を読み込んで、デプロイ(またはアップデート)を実行します。

source kms-aws2.env 

aws cloudformation update-stack \
  --template-body file://kms.yaml \
  --stack-name "${AccountName}-kms-stack" \
  --parameters \
    ParameterKey=AccountName,ParameterValue=$AccountName \
    ParameterKey=Aws1AccountId,ParameterValue=$Aws1AccountId \
    ParameterKey=Aws1ReplicationRoleName,ParameterValue=$Aws1ReplicationRoleName \
  --capabilities CAPABILITY_NAMED_IAM

※マネジメントコンソール等で、KMSのキーポリシーに上記の設定が正しく追加されていることを確認してください。

2. S3バケット作成 & バケットポリシー設定(AWS2)

次に、AWS2側にデータを受け取るバケットを作ります。レプリケーションの必須条件である「バージョニングの有効化」と、AWS1からの書き込みを許可する「バケットポリシー」を同時に流し込みます。

まず、AWS2用の環境変数ファイル s3-aws2.env を作成します。

cat << 'EOF' > s3-aws2.env
AccountName=aws2
BucketName=aws2-bucket-om-xxxx-1234-5678-9012
Aws1AccountId=123456789012
Aws1ReplicationRoleName=aws1-s3-replication-role
EOF

続いて、以下の s3.yaml を作成します。

AWSTemplateFormatVersion: '2010-09-09'
Description: 'S3 Bucket with KMS Encryption and Bucket Policy (Test)'

Parameters:
  AccountName:
    Type: String
  BucketName:
    Type: String
  Aws1AccountId:
    Type: String
    Description: 'AWS1 12-digit Account ID'
  Aws1ReplicationRoleName:
    Type: String
    Description: 'IAM Role Name for replication in AWS1'

Resources:
  # S3バケットの作成
  TestS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName

      # レプリケーションの必須条件「バージョニング」を有効化!
      VersioningConfiguration:
        Status: Enabled
      
      # デフォルトの暗号化設定(KMS連動)
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - BucketKeyEnabled: true
            ServerSideEncryptionByDefault:
              SSEAlgorithm: 'aws:kms'
              KMSMasterKeyID: !Sub "alias/${AccountName}-s3-key"

  # ------------------------------------------------------------
  # AWS1からのレプリケーションを受け入れるバケットポリシー
  # ------------------------------------------------------------
  TestS3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref TestS3Bucket # 上のバケットにこのポリシーを紐付け
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AllowAWS1Replication
            Effect: Allow
            Principal:
              # AWS1のレプリケーションロールをピンポイントで信頼!
              AWS: !Sub "arn:aws:iam::${Aws1AccountId}:role/${Aws1ReplicationRoleName}"
            Action:
              - 's3:ReplicateObject'
              - 's3:ReplicateDelete'
              - 's3:ReplicateTags'
            Resource: !Sub "arn:aws:s3:::${BucketName}/*" # バケットの中身すべてへの書き込みを許可

デプロイを実行します。

# 1. AWS2用の環境変数を読み込み
source s3-aws2.env

# 2. S3スタックをアップデート!
aws cloudformation update-stack \
  --template-body file://s3.yaml \
  --stack-name "${AccountName}-s3-stack" \
  --parameters \
    ParameterKey=AccountName,ParameterValue=$AccountName \
    ParameterKey=BucketName,ParameterValue=$BucketName \
    ParameterKey=Aws1AccountId,ParameterValue=$Aws1AccountId \
    ParameterKey=Aws1ReplicationRoleName,ParameterValue=$Aws1ReplicationRoleName

デプロイ完了後、S3のバージョニングがしっかり「有効」になっていることを確認してください。

【フェーズ2】AWS1(送信元)の設定と連携

ここからアカウントを AWS1(送信元) に切り替えます。

1. 環境変数ファイルの準備(AWS1)

AWS2の情報をAWS1側に教えるため、環境変数ファイル(s3-aws1.env)にAWS2のバケット名やKMSの「本物のキーARN」を追記します。

cat << 'EOF' > s3-aws1.env
AccountName=aws1
BucketName=aws1-bucket-om-xxxx-1234-5678-9012
Aws2AccountId=987654321098                      # AWS2の12桁アカウントID
Aws2BucketName=aws2-bucket-om-xxxx-1234-5678-9012  # AWS2のバケット名
Aws2KmsKeyArn=arn:aws:kms:ap-northeast-1:987654321098:key/yyyy-yyyy-yyyy-yyyy # ★AWS2で作った本物のKMSキーARN
EOF

2. レプリケーション設定付きS3バケットの作成(AWS1)

AWS1側の s3.yaml を以下のように修正します。 ここでは、「レプリケーション用IAMロールの作成」と、KMS暗号化オブジェクトをコピーするための「ReplicationConfiguration」をバケットに組み込みます。

AWSTemplateFormatVersion: '2010-09-09'
Description: 'S3 Bucket with KMS Encryption and Replication (Test)'

Parameters:
  AccountName:
    Type: String
  BucketName:
    Type: String
  Aws2AccountId:
    Type: String
  Aws2BucketName:
    Type: String
  Aws2KmsKeyArn:
    Type: String

Resources:
  # ------------------------------------------------------------
  # 1. レプリケーション用のIAMロール
  # ------------------------------------------------------------
  S3ReplicationRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: aws1-s3-replication-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: s3.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: S3ReplicationPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              # 送信元(AWS1)バケットの読み込み権限
              - Effect: Allow
                Action:
                  - s3:GetReplicationConfiguration
                  - s3:ListBucket
                Resource: !Sub "arn:aws:s3:::${BucketName}"
              - Effect: Allow
                Action:
                  - s3:GetObjectVersionForReplication
                  - s3:GetObjectVersionAcl
                  - s3:GetObjectVersionTagging
                Resource: !Sub "arn:aws:s3:::${BucketName}/*"
              
              # 送信先(AWS2)バケットへの書き込み権限
              - Effect: Allow
                Action:
                  - s3:ReplicateObject
                  - s3:ReplicateDelete
                  - s3:ReplicateTags
                Resource: !Sub "arn:aws:s3:::${Aws2BucketName}/*"
              
              # ★スマート化:AWS1のKMS ARNを入力させず、自身の環境情報から自動組み立て!
              - Effect: Allow
                Action:
                  - kms:Decrypt
                Resource: !Sub "arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/*"
              
              # AWS2の鍵(相手)に対する暗号化権限
              - Effect: Allow
                Action:
                  - kms:Encrypt
                  - kms:GenerateDataKey
                Resource: !Ref Aws2KmsKeyArn

  # ------------------------------------------------------------
  # 2. S3バケットの作成とレプリケーションルールの統合
  # ------------------------------------------------------------
  TestS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      
      # レプリケーションの必須条件「バージョニング」を有効化
      VersioningConfiguration:
        Status: Enabled
      
      # デフォルトの暗号化設定(KMS連動)
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - BucketKeyEnabled: true
            ServerSideEncryptionByDefault:
              SSEAlgorithm: 'aws:kms'
              KMSMasterKeyID: !Sub "alias/${AccountName}-s3-key"
              
      # 自動コピールールの設定
      ReplicationConfiguration:
        Role: !GetAtt S3ReplicationRole.Arn
        Rules:
          - Id: CrossAccountReplicationRule
            Status: Enabled
            Priority: 1
            DeleteMarkerReplication:
              Status: Enabled
            Filter:
              Prefix: ""
            Destination:
              Bucket: !Sub "arn:aws:s3:::${Aws2BucketName}"
              Account: !Ref Aws2AccountId
              # KMS暗号化されたオブジェクトをレプリケーション対象にする設定
              SourceSelectionCriteria:
                SseKmsEncryptedObjects:
                  Status: Enabled
              EncryptionConfiguration:
                ReplicaKmsKeyID: !Ref Aws2KmsKeyArn

環境変数を読み込み、AWS1側でデプロイを実行します。

source s3-aws1.env

aws cloudformation update-stack \
  --template-body file://s3.yaml \
  --stack-name "${AccountName}-s3-stack" \
  --parameters \
    ParameterKey=AccountName,ParameterValue=$AccountName \
    ParameterKey=BucketName,ParameterValue=$BucketName \
    ParameterKey=Aws2AccountId,ParameterValue=$Aws2AccountId \
    ParameterKey=Aws2BucketName,ParameterValue=$Aws2BucketName \
    ParameterKey=Aws2KmsKeyArn,ParameterValue=$Aws2KmsKeyArn \
  --capabilities CAPABILITY_NAMED_IAM

【疎通テスト】

すべてのデプロイが完了したら、AWS1のバケットに新しくテストファイル(例: test5.txt)をアップロードしてみましょう。 ※レプリケーションは、設定後に新しく置かれたオブジェクトのみが対象になります。

アップロード後、AWS1のCLIから以下の確認コマンドを実行します。

aws s3api head-object --bucket $BucketName --key test5.txt --query ReplicationStatus --output text

最初は PENDING(処理中)と表示されることがありますが、数秒〜数十秒待って再度実行し、画面に以下の文字が返ってくれば大成功です!

AWS2(送信先)のバケットを確認すると、AWS1のKMSで暗号化されていたファイルが、AWS2のKMSキーで綺麗に再暗号化されて格納されているはずです。

まとめ

KMS暗号化が絡むクロスアカウントレプリケーションをスムーズに成功させる鍵は、「AWS2(送信先)の受け入れ態勢(KMSキーポリシーのCondition縛り・バケットポリシー)を先に完璧に作ること」です。

この手順通りに進めれば、循環参照のようなエラーや FAILED 地獄に悩されることなく、綺麗にCloudFormationでの自動化が完了します。マルチアカウント運用の自動化・バックアップ体制の構築にぜひ役立ててください!