How to Set Up AWS S3 Bucket Cross-Account Replication Using CDK
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
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!