AWS CDK: Cross-Region S3 Replication with KMS

June 6th, 2021 977 Words

AWS has everything you need for secure and reliable data storage. With Amazon S3, you can easily build a low-cost and high-available solution. Together with the available features for regional replication, you can easily have automatic cross-region backups for all data in S3.

Overview

This example is a CDK project in TypeScript. Together with CloudFormation StackSets, you can deploy all resources in all needed regions with a single command:

  • S3 Bucket in primary region with custom KMS key
  • CloudFormation StackSet for regional replications
  • IAM Role with access for primary region and replications

Besides the default IAM Roles for CloudFormation Stacks, you only use managed AWS resources and no custom source code or data processing:

AWS Architecture

The CDK project is available on GitHub. It’s ready-to-use and you can easily configure all parameters for the CloudFormation stack:

// Configured ./aws/index.ts

const option = {
  env: {
    region: 'eu-central-1'
  },
  replications: [
    'eu-west-1',
    'eu-north-1'
  ]
}

Afterwards, run npx cdk deploy to deploy everything.

Primary Region and Data Source

All changes to data in the Amazon S3 Bucket in your primary AWS region are replicated to additional AWS regions. In the primary region, you need a Amazon S3 Bucket and a custom KMS key used for encryption.

import * as s3 from '@aws-cdk/aws-s3'
import * as kms from '@aws-cdk/aws-kms'

const key = new kms.Key(this, 'Key')
const alias = key.addAlias('archive')

const bucket = new s3.Bucket(this, 'Bucket', {
  bucketName: `${props.prefix}-archive`,
  encryption: s3.BucketEncryption.KMS,
  encryptionKey: alias,
  blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
  bucketKeyEnabled: true,
  versioned: true,
  removalPolicy: cdk.RemovalPolicy.RETAIN
})

To use S3 bucket replication, you need to create an IAM Role with the permissions to access data in S3 and use your KMS key:

import * as iam from '@aws-cdk/aws-iam'

const role = new iam.Role(this, 'ReplicationRole', {
  assumedBy: new iam.ServicePrincipal('s3.amazonaws.com'),
  path: '/service-role/'
});


role.addToPolicy(
  new iam.PolicyStatement({
    resources: [
      bucket.bucketArn
    ],
    actions: [
      's3:GetReplicationConfiguration',
      's3:ListBucket'
    ]
  })
);

role.addToPolicy(
  new iam.PolicyStatement({
    resources: [
      bucket.arnForObjects('*')
    ],
    actions: [
      's3:GetObjectVersion',
      's3:GetObjectVersionAcl',
      's3:GetObjectVersionForReplication',
      's3:GetObjectVersionTagging'
    ]
  })
);

role.addToPolicy(
  new iam.PolicyStatement({
    resources: [
      key.keyArn
    ],
    actions: [
      'kms:Decrypt'
    ]
  })
);

With all that in place, the next step is to create an Amazon S3 Bucket and KMS key in all regions you want to use for replication.

CloudFormation StackSet

To avoid creating individual CloudFormation stacks in every region you want to use for replication, you can use a CloudFormation StackSet to automate the regional deployments. Currently, the AWS Cloud Development Kit only supports the low-level access to CloudFormation StackSet resources:

import * as cdk from '@aws-cdk/core'

new cdk.CfnStackSet(this, "StackSet", {
  stackSetName: `${props.prefix}-archive-replication`,
  permissionModel: "SELF_MANAGED",
  parameters: [
    {
      parameterKey: 'Prefix',
      parameterValue: props.prefix
    },
    {
      parameterKey: 'ReplicationRole',
      parameterValue: role.roleArn
    }
  ],
  stackInstancesGroup: [
    {
      regions: props.replications,
      deploymentTargets: {
        accounts: [this.account],
      },
    },
  ],
  templateBody:templateReplicationData,
});

The templateReplicationData is a CloudFormation template containing the Amazon S3 and KMS resources for every region. The parameter ReplicationRole is need to grant access to the regional KMS key for the IAM Role used for replication.

Parameters:
  Prefix:
    Type: String
  ReplicationRole:
    Type: String

Resources:
  Key:
    Type: AWS::KMS::Key
    Properties:
      KeyPolicy:
        Version: 2012-10-17
        Id: access-account
        Statement:
          - Sid: Enable IAM User Permissions
            Effect: Allow
            Principal:
              AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
            Action: kms:*
            Resource: '*'
          - Sid: Replication
            Effect: Allow
            Principal:
              AWS: !Ref ReplicationRole
            Action:
              - kms:Encrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
              - kms:DescribeKey
            Resource: '*'

  KeyAlias:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: alias/archive/replication
      TargetKeyId: !Ref Key

  Bucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    Properties:
      BucketName: !Sub ${Prefix}-archive-replication-${AWS::Region}
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: Yes
        BlockPublicPolicy: Yes
        IgnorePublicAcls: Yes
        RestrictPublicBuckets: Yes
      VersioningConfiguration:
        Status: Enabled
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - BucketKeyEnabled: Yes
          ServerSideEncryptionByDefault:
            SSEAlgorithm: aws:kms
            KMSMasterKeyID: !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:${KeyAlias}

The AWS resources in the replication regions use a specific name pattern to reference them in the next configuration. For example, the regional resources for eu-north-1 have these names and IDs:

  • KMS Key
    arn:aws:kms:eu-north-1:1234:alias/archive/replication
  • S3 Bucket
    arn:aws:s3:::prefix-archive-replication-eu-north-1

With this naming pattern, you can now extend the IAM Role used for replication in your primary region:

role.addToPolicy(
  new iam.PolicyStatement({
    resources: props.replications.map(
      region => `arn:aws:kms:${region}:${this.account}:alias/archive/replication`
    ),
    actions: [
      'kms:Encrypt'
    ]
  })
);

role.addToPolicy(
  new iam.PolicyStatement({
    resources: props.replications.map(
      region => `arn:aws:s3:::${props.prefix}-archive-replication-${region}/*`
    ),
    actions: [
      's3:ReplicateDelete',
      's3:ReplicateObject',
      's3:ReplicateTags'
    ]
  })
);

role.addToPolicy(
  new iam.PolicyStatement({
    resources: props.replications.map(
      region => `arn:aws:s3:::${props.prefix}-archive-replication-${region}`
    ),
    actions: [
      's3:List*',
      's3:GetBucketVersioning',
      's3:PutBucketVersioning'
    ]
  })
);

Without the naming schema, you would not be able to accomplish this. The IAM Role needs access to the S3 Bucket and KMS Key in every region, but you cannot reference objects in a CloudFormation StackSet using CloudFormation templates or the CDK.

Last, but not least, you need to configure the S3 Replication Configuration for the S3 bucket in your primary location. The AWS CloudFormation Development Kit has no higher-level construct yet, but you can still use the low-level objects to configure replication:

const cfnBucket = bucket.node.defaultChild as s3.CfnBucket;

cfnBucket.replicationConfiguration = {
  role: role.roleArn,
  rules: props.replications.map(
    (region, index) => (
      {
        id: region,
        destination: {
          bucket: `arn:aws:s3:::${props.prefix}-archive-replication-${region}`,
          encryptionConfiguration: {
            replicaKmsKeyId: `arn:aws:kms:${region}:${this.account}:alias/archive/replication`
          }
        },
        priority: index,
        deleteMarkerReplication: {
          status: 'Enabled'
        },
        filter: {
          prefix: ''
        },
        sourceSelectionCriteria: {
          sseKmsEncryptedObjects: {
            status: 'Enabled'
          }
        },
        status: 'Enabled'
      }
    )
  )
}

Again, this only works with hard-coded names and the pattern of predefined names for S3 buckets and KMS keys.

Summary

This uses the AWS Cloud Development Kit to create an AWS CloudFormation template to create an AWS CloudFormation stack. The AWS CloudFormation stack creates an Amazon S3 bucket, an AWS Identity & Access Management role, an AWS Key Management Service key and an AWS CloudFormation StackSet. The AWS CloudFormation StackSet uses an AWS CloudFormation template to create an AWS CloudFormation stack in all AWS regions. The AWS CloudFormation stack in every region creates an Amazon S3 bucket and an AWS Key Management Service key. You’re welcome. 🤯

The complex orchestration is need to deploy all resources with a single npx cdk deploy command. Of course, there are other ways to achieve this, but if you want to only rely on AWS services, have no custom code, and use a single deployment state, use this!


View on GitHub Source code is published using the MIT License.