How to Set Up AWS S3 Bucket Cross-Account Replication Using CDK

Andres CubillosAndres Cubillos
3 min read

Recently, I received a requirement to replicate files from one S3 bucket to another in a different AWS account. While setting this up through the AWS Console was relatively straightforward, I found it more challenging to implement using the AWS Cloud Development Kit (CDK).

In this post, Iโ€™ll walk you through the exact steps I followed, so you can easily set up cross-account replication using CDK for your projects

Github repo at the end of this post. ๐Ÿ˜‰

Create Destination S3 Bucket

import * as cdk from "aws-cdk-lib";
import { BlockPublicAccess, Bucket, ObjectOwnership } from "aws-cdk-lib/aws-s3";
import { Construct } from "constructs";
import { ParameterNames } from "../properties/parameters-names";

export class DestinationS3BucketStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);

    const destinationBucket = new Bucket(
      this,
      `${ParameterNames.PROJECT_NAME}-bucket-id`,
      {
        bucketName: `${ParameterNames.PROJECT_NAME}-${props.env?.account}`,
        blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
        removalPolicy: cdk.RemovalPolicy.DESTROY,
        autoDeleteObjects: true,
        //๐Ÿ‘‡ enforce bucket ownership to manipulate replicated objects
        objectOwnership: ObjectOwnership.BUCKET_OWNER_ENFORCED, 
        // ๐Ÿ‘‡ Source and destination buckets should have Versioning enabled
        versioned: true,
      }
    );
  }
}

Create Origin S3 Bucket

const sourceBucket = new Bucket(
      this,
      `${ParameterNames.PROJECT_NAME}-bucket-id`,
      {
        bucketName: `${ParameterNames.PROJECT_NAME}-${props.env?.account}`,
        blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
        removalPolicy: cdk.RemovalPolicy.DESTROY,
        autoDeleteObjects: true,
        // ๐Ÿ‘‡ Source and destination buckets should have Versioning enabled
        versioned: true,
      }
    );
  • Create a replication role and the permissions to access the destination bucket
const replicationRoleSource = new Role(
      this,
      `${ParameterNames.PROJECT_NAME}-ReplicationRole-id`,
      {
        roleName: `${ParameterNames.PROJECT_NAME}-ReplicationRole`,
        assumedBy: new ServicePrincipal("s3.amazonaws.com"),
      }
    );

    replicationRoleSource.addToPolicy(
      new PolicyStatement({
        actions: [
          "s3:ListBucket",
          "s3:GetReplicationConfiguration",
          "s3:GetObjectVersionForReplication",
          "s3:GetObjectVersionAcl",
          "s3:GetObjectVersionTagging",
          "s3:GetObjectRetention",
          "s3:GetObjectLegalHold",
        ],
        effect: Effect.ALLOW,
        resources: [
          sourceBucket.bucketArn,
          `${sourceBucket.bucketArn}/*`,
          `${ParameterNames.DESTINATION_BUCKET_ARN}`,
          `${ParameterNames.DESTINATION_BUCKET_ARN}/*`,
        ],
      })
    );

    replicationRoleSource.addToPolicy(
      new PolicyStatement({
        actions: [
          "s3:ReplicateObject",
          "s3:ReplicateDelete",
          "s3:ReplicateTags",
          "s3:ObjectOwnerOverrideToBucketOwner",
        ],
        effect: Effect.ALLOW,
        resources: [
          `${sourceBucket.bucketArn}/*`,
          `${ParameterNames.DESTINATION_BUCKET_ARN}/*`,
        ],
      })
    );
  • add a replication rule to transfer the objects from source to destination
const cfnBucket = sourceBucket.node.defaultChild as CfnBucket;
    cfnBucket.replicationConfiguration;

    cfnBucket.replicationConfiguration = {
      role: replicationRoleSource.roleArn,
      rules: [
        {
          id: "replication-rule",
          priority: 0,
          filter: {
            // add a prefix if you want to replicate specific objects. 
            // Leave blank to replicate all 
            // ๐Ÿ‘‡
            prefix: "", 
          },
          deleteMarkerReplication: {
            status: "Enabled",
          },
          destination: {
            account: ParameterNames.DESTINATION_ACCOUNT,
            bucket: ParameterNames.DESTINATION_BUCKET_ARN,
            accessControlTranslation: {
              owner: "Destination", // ๐Ÿ‘ˆ Destination or BucketOwner 
            },
          },
          status: "Enabled",
        },
      ],
    };

Modify destination bucket policy

  • we should modify the destination bucket policy to allow access from the replication role in the source Account to perform some operations in the destination Account.
// ๐Ÿ‘‡ Get the destination bucket previoisly created 
    const destinationBucket = Bucket.fromBucketName(this, 'destination-bucket-id', ParameterNames.DESTINATION_BUCKET_NAME);

    // ๐Ÿ‘‡ Create a new bucket policy for your destination bucket
    const bucketPolicy = new BucketPolicy(this, 'bucket-policy-id', {
      bucket: destinationBucket, // ๐Ÿ‘ˆ Pass the destination bucket
    });

    // ๐Ÿ‘‡ Add a statement to the bucket
    bucketPolicy.document.addStatements(
      new PolicyStatement({
        effect: Effect.ALLOW,
        principals: [new ArnPrincipal(ParameterNames.SOURCE_ROLE_ARN)],
        actions: [
                  "s3:GetBucketVersioning",
                  "s3:PutBucketVersioning",
                  "s3:List*"
        ],
        resources: [destinationBucket.bucketArn],
      }),
    );

    bucketPolicy.document.addStatements(
      new PolicyStatement({
        effect: Effect.ALLOW,
      principals: [new ArnPrincipal(ParameterNames.SOURCE_ROLE_ARN)],
      actions: [
                "s3:ReplicateObject",
                "s3:ReplicateDelete",
                "s3:ObjectOwnerOverrideToBucketOwner"
      ],
      resources: [`${destinationBucket.bucketArn}/*`],
      }),
    );

    bucketPolicy.document.addStatements(
      new PolicyStatement({
        effect: Effect.ALLOW,
      principals: [new ArnPrincipal(ParameterNames.SOURCE_ROLE_ARN)],
      actions: [
                "s3:GetBucketVersioning",
                "s3:PutBucketVersioning",
                "s3:List*"
      ],
      resources: [destinationBucket.bucketArn],
      }),
    );

Test

Upload a file to the source bucket and observe its replication in the destination bucket.

Click on the upload object to verify the replication status.

Finally, check the destination bucket to verify that the files were replicated.

I hope you find this helpful. Thanks for reading this post! ๐Ÿ˜Ž

Github Repo : https://github.com/felipecubillos/cdk-s3-bucket-cross-account-replication

4
Subscribe to my newsletter

Read articles from Andres Cubillos directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Andres Cubillos
Andres Cubillos

I'm a Software Developer who loves diving into the backend world, especially with AWS Services. You'll often find me coding away in Java, Javascript, and Python. Lately, I've been exploring the frontend world too, playing around with React. I'm all about sharing what I learn and spreading good vibes in the tech community!