Skip to content

s3: replication only allows a single source bucket #33355

@jochemd

Description

@jochemd

Describe the bug

v2.177.0 introduced the ability to set up S3 replication. This supports replicating from one source bucket to multiple destination buckets. But as soon as you configure a second source bucket, the stack fails to deploy (synth and diff work fine) with the error: CDKReplicationRole already exists ...

This appears to be caused by the replication feature using a role with a hardcoded name without any checks if a role by that name already exists.

Regression Issue

  • Select this option if this issue appears to be a regression.

Last Known Working CDK Version

No response

Expected Behavior

Deployment does not generate any errors and all source buckets are set up with functioning replication.

Current Behavior

During deployment an error occurs:
CDKReplicationRole already exists in stack arn:aws:cloudformation:eu-west-1:xxxxxxxxxxxx:stack/test-BucketsInfraStack/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Reproduction Steps

#!/usr/bin/env node
import { App } from 'aws-cdk-lib'
import { BucketsStage } from '../lib/BucketsStage'

const app = new App()
new BucketsStage(app, 'test', {})
app.synth()
import { Stage, StageProps } from 'aws-cdk-lib'
import { Construct } from 'constructs'
import { BucketsDrStack } from './BucketsDRStack'
import { BucketsInfraStack } from './BucketsInfraStack'

export class BucketsStage extends Stage {
  constructor(scope: Construct, id: string, props: StageProps) {
    super(scope, id, props)

    /**
     * Disaster recovery to a different region
     */
    const bucketsDrStack = new BucketsDrStack(this, 'BucketsDrStack', {
      env: {
        account: 'xxxxxxxxxxxx',
        region: 'eu-central-1'
      },
      crossRegionReferences: true,
      description: 'Disaster recovery to a different region'
    })

    /**
     * Infra stack sets up the basic and statefull infra
     */
    new BucketsInfraStack(this, 'BucketsInfraStack', {
      env: {
        account: 'xxxxxxxxxxxx',
        region: 'eu-west-1'
      },
      bucketsDrStack,
      crossRegionReferences: true,
      description: 'Infra stack sets up the basic and statefull infra'
    })
  }
}
import { RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'
import { BlockPublicAccess, Bucket, BucketEncryption, IBucket } from 'aws-cdk-lib/aws-s3'
import type { Construct } from 'constructs'

export class BucketsDrStack extends Stack {
  readonly bucketAReplica: IBucket
  readonly bucketBReplica: IBucket

  constructor (scope: Construct, id: string, props: StackProps) {
    super(scope, id, props)

    this.bucketAReplica = new Bucket(this, 'bucketAReplica', {
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
      encryption: BucketEncryption.S3_MANAGED,
      enforceSSL: true,
      versioned: true,
      removalPolicy: RemovalPolicy.DESTROY
    })

    this.bucketBReplica = new Bucket(this, 'bucketBReplica', {
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
      encryption: BucketEncryption.S3_MANAGED,
      enforceSSL: true,
      versioned: true,
      removalPolicy: RemovalPolicy.DESTROY
    })
  }
}
import {
  RemovalPolicy,
  Stack,
  StackProps
} from 'aws-cdk-lib'
import { BlockPublicAccess, Bucket, BucketEncryption } from 'aws-cdk-lib/aws-s3'
import type { Construct } from 'constructs'
import { BucketsDrStack } from './BucketsDRStack'

export interface OrInfraStackProps extends StackProps {
  bucketsDrStack: BucketsDrStack;
}

export class BucketsInfraStack extends Stack {
  readonly privateBucket: Bucket
  readonly publicBucket: Bucket

  constructor(
    scope: Construct,
    id: string,
    props: OrInfraStackProps
  ) {
    super(scope, id, props)

    const { bucketsDrStack } = props

    // The S3 bucket for the private assets
    this.privateBucket = new Bucket(this, 'PrivateBucket', {
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
      encryption: BucketEncryption.S3_MANAGED,
      enforceSSL: true,
      versioned: true,
      replicationRules: [{
        deleteMarkerReplication: true,
        destination: bucketsDrStack.bucketAReplica,
        priority: 1,
      }],
      removalPolicy: RemovalPolicy.DESTROY
    })

    // The S3 bucket for the public (accessible through CloudFront) assets
    this.publicBucket = new Bucket(this, 'PublicBucket', {
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
      encryption: BucketEncryption.S3_MANAGED,
      enforceSSL: true,
      versioned: true,
      /* uncommenting the next lines will cause the error
      replicationRules: [{
        deleteMarkerReplication: true,
        destination: bucketsDrStack.bucketBReplica,
        priority: 1,
      }],
      */
      removalPolicy: RemovalPolicy.DESTROY
    })
  }
}

Possible Solution

The source bucket should accept an explicit replication role and add permissions to it instead of creating a role.

Additional Information/Context

No response

CDK CLI Version

2.178.1 (build ae342cb)

Framework Version

No response

Node.js Version

v22.13.0

OS

Linux

Language

TypeScript

Language Version

5.7.3

Other information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    @aws-cdk/aws-s3Related to Amazon S3bugThis issue is a bug.effort/mediumMedium work item – several days of effortp2

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions