AWS CDK and Amazon DynamoDB global tables
Tags: cdk, dynamodb, serverless, aws
Published: 2022-09-02 03:55:20

---
title: AWS CDK and Amazon DynamoDB global tables
published: true
description: The "regular" way of creating global tables causes toil. What we can do about it?
tags: cdk, dynamodb, serverless, aws
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uof067xmbppnkas3865g.jpg
---
Amazon DynamoDB is THE database for serverless AWS applications. Its HTTP-based connection model makes integrating with serverless computing services like AWS Lambda easy. One of the additional capabilities of Amazon DynamoDB is the [_global tables_](https://aws.amazon.com/dynamodb/global-tables/), which allows you to run replica instances of the database in other regions.

In this blog post, I will touch on Amazon DynamoDB global tables in the context of the [AWS CDK](https://aws.amazon.com/cdk/) framework – there are a couple of nuances with huge implications you should be aware of.

Let us dive in.

## Traditional way of creating global tables

There are two ways I'm aware of to create global tables using AWS CDK, with one of them being, in my humble opinion, much better for the maintainability of your application.

Let us start with the "traditional" way of creating the Amazon DynamoDB global tables via the AWS CDK – by using the [`aws_dynamodb.Table` construct](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_dynamodb.Table.html) and specifying the `replicationRegions` property.

```ts
const globalTable = new dynamodb.Table(this, "Table", {
  partitionKey: { name: "id", type: dynamodb.AttributeType.STRING },
  replicationRegions: ["us-east-1", "us-east-2", "us-west-2"],
  billingMode: dynamodb.BillingMode.PROVISIONED
});
```

Here I've defined a table with three replication regions. If you deploy this code snippet in the AWS DynamoDB web console, you will see that the table has three replicas – one for each region.

On the surface, all looks good and works as expected. But as soon as you peek "under the curtains" of the construct, you will see something unexpected – that deploying the "Table" created an [AWS CloudFormation custom resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) alongside the DynamoDB resource.

### The problem with the custom resource

> You can find the code the custom resource in question uses [here](https://github.com/aws/aws-cdk/blob/c607ca51be1042f091b4e4419f20bec75863055c/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts).

The custom resource is responsible for creating the replica tables. It uses the AWS SDK `updateTable` API and provides the appropriate value for the `ReplicaUpdates` property. On the surface, it does not sound bad, but **by creating the replicas via the AWS SDK call, we effectively detach those resources from your AWS CloudFormation template and AWS CDK code**.

![Simplified diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8a3r9roj77cgia53j6zz.jpg)

The implications of this architecture are enormous from the maintainability perspective. Here are the two cases of confusion and toil I had to deal with multiple times when operating global tables deployed via the `replicationRegions` property.

#### The `DeletionPolicy` attribute

If you are running a production workload, it is considered good practice (I would argue a must-have) to specify [the `DeletionPolicy`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-deletionpolicy.html) (in AWS CDK, the name is `removalPolicy`) of `RETAIN` for resources that are stateful and hold production data.

If you delete the CloudFormation stack, the CloudFormation will **not** delete the resources when we set the `DeletionPolicy` to `RETAIN`. As you can imagine, this can save you from a disaster.

Usually, in AWS CDK, this is done by using the `applyRemovalPolicy` method on a given construct.

```ts
const globalTable = new cdk.aws_dynamodb.Table(this, "Table", {
  // ...
});
globalTable.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN);
```

The problem is that **replicas created via the custom resource will not inherit that `removalPolicy`. This setting will only apply to the "root" table**.

So, If I were to delete a CloudFormation stack, the CloudFormation will preserve the "root" table but delete the custom resource. **Since the custom resource triggered with a `Delete` event, it would issue the `Delete` action for all the replica tables**. And with that, all of your replica tables are gone.

To make sure the `Delete` event never gets to the custom resource the AWS CDK uses, you have to set the `DeletionPolicy` on the custom resource itself. That is not so easy as you have to "find" the custom resource node in the AWS CDK tree and override its `DeletionPolicy` attribute. Here is how I would do it.

```ts
const globalTable = new cdk.aws_dynamodb.Table(this, id, {
  // ...
});

/**
 * This only applies to the "root table" and NOT THE REPLICAS!
 */
globalTable.applyRemovalPolicy(removalPolicy);

/**
 * Make sure we do not remove the custom resource
 */
const customReplicaResource = globalTable.node.children.find(child => {
  return (child as any).resource?.cfnResourceType === "Custom::DynamoDBReplica";
}) as cdk.CfnResource;

customReplicaResource.applyRemovalPolicy(removalPolicy);
```

#### Enabling point-in-time recovery on the replica tables

For production usage, enabling _point-in-time recovery_ (PITR) is arguably a must. It is not only helpful in data-corruption scenarios but also for disaster recovery. In CDK, applying the PITR on a global table (created via the `replicationRegions` property) might initially seem easy.

```ts
const globalTable = new cdk.aws_dynamodb.Table(this, id, {
  pointInTimeRecovery: true
  // ...
});
```

The problem is, similarly to the `DeletionPolicy` we have looked at earlier, that **the `pointInTimeRecovery` property only applies to the "root" table. It will NOT be "forwarded" to the replica tables**. An auditing tool will catch that for you in the best-case scenario. In the worst, you will not be able to recover your replica tables if something goes wrong.

All hope is not lost, though. You could also use a workaround to ensure that the replica tables have the same PITR setting as your "root" table. This involves calling `updateContinuousBackups` yourself via the `AwsCustomResource` construct for each region where the replica resides.

```ts
const replicationRegions = ["eu-center-1", "eu-west-1"];

const globalTable = new cdk.aws_dynamodb.Table(this, id, {
  pointInTimeRecovery: true,
  replicationRegions
  // ...
});

for (const replicationRegion of replicationRegions) {
  new cdk.custom_resources.AwsCustomResource(this, id, {
    onUpdate: {
      service: "DynamoDB",
      action: "updateContinuousBackups",
      parameters: {
        TableName: globalTable.tableName,
        PointInTimeRecoverySpecification: {
          PointInTimeRecoveryEnabled: true
        }
      },
      region: replicationRegion,
      physicalResourceId: cdk.custom_resources.PhysicalResourceId.of(id)
    },
    policy: cdk.custom_resources.AwsCustomResourcePolicy.fromSdkCalls({
      resources: [globalTable.tableArn]
    })
  });
}
```

## A different way of creating global tables

Instead of using the `aws_dynamodb.Table` construct, **consider using the `aws_dynamodb.CfnGlobalTable`**. To the best of my knowledge, this resource was made available to us in May 2021, and I would highly encourage you for it to be your default, **even if you do not anticipate using replica tables in the immediate future in your architecture**.

> If you want to dive deeper into technical details about the resource to which `CfnGlobalTable` corresponds, look no further than [this excellent blog post](https://acloudguru.com/blog/engineering/you-should-always-use-dynamodb-global-tables-now).

I advocate for the `CfnGlobalTable` usage because the gotchas mentioned in the previous section do not apply to this construct. For example, to specify the `DeletionPolicy`, which would apply to **all tables, even the replicas**, all you need to do is to use the `applyRemovalPolicy` method available on the construct.

```ts
const globalTable = new cdk.aws_dynamodb.CfnGlobalTable(this, id, {
  // ...
});
globalTable.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN);
```

What about the PITR? To enable it on all the tables (replicas and the "root" table) do not need to fiddle with AWS SDK calls through a custom resource (or some other way). You can set the PITR for the "root" table and the replica tables.

```ts
const globalTable = new cdk.aws_dynamodb.CfnGlobalTable(this, id, {
  // ...
  replicas: [
    {
      region: "eu-center-1",
      pointInTimeRecoverySpecification: {
        pointInTimeRecoveryEnabled: true
      }
    },
    // Let us say that, the root table lives in "eu-west-1"
    {
      region: "eu-west-1",
      pointInTimeRecoverySpecification: {
        pointInTimeRecoveryEnabled: true
      }
    }
  ]
});
```

Another significant reason for using the `CfnGlobalTable` is that **this resource uses a newer version of Amazon DynamoDB global tables**. You can read about different versions of global tables [here](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/globaltables.DetermineVersion.html).

### Migration

I'm not yet ready to publish a migration story here (I'm still evaluating different avenues to do so), so please keep an eye on my next article. If you want to perform migration now, I would base the steps necessary on [this](https://awstip.com/converting-a-dynamodb-single-region-table-to-a-global-table-with-zero-downtime-151c018cbef0) and [this](https://matthewbonig.com/2021/08/30/importing-with-the-cdk/) article.

## Closing words

I hope that the information in this article will save you some toil and frustrations I had to go through while working with global tables created via the `replicationRegions` property in CDK.

As I mentioned, the `CfnGlobalTable` is objectively better suited for the job. Yes, you might lose some of the niceties of the L2 (L3?) CDK construct, but using the newer version of the Amazon DynamoDB global tables will pay dividends in the long run.

For more similar content, please consider following me on Twitter – [@wm_matuszewski](https://twitter.com/wm_matuszewski).

Thank you for your time.