A new and improved AWS CDK construct for Amazon DynamoDB tables
February 1, 2024Recently, we launched a new AWS Cloud Development Kit (CDK) construct for Amazon DynamoDB tables, known as TableV2. This construct provides a number of new features in addition to what the original construct offered, enabling CDK authors to create global tables, simplifying the configuration of global secondary indexes and auto scaling, as well as supporting AWS CloudFormation drift detection and import operations. We believe that this new construct will make it easier for organizations to build and manage their DynamoDB tables at scale, in addition to providing more flexibility and control over the configuration of tables.
AWS CDK is a framework for defining cloud infrastructure in code and provisioning it through AWS CloudFormation. Developers can use any of the supported programming languages to define reusable cloud components known as Constructs. A construct is a reusable and programmable component that represents AWS resources. CDK translates the high-level constructs defined by you into equivalent AWS CloudFormation templates. CloudFormation provisions the resources specified in the template, streamlining the usage of Infrastructure as a Code (IaC) on AWS.
In this post we’ll explore:
- The reasoning behind the creation of a new L2 construct for DynamoDB tables.
- Features of new L2 constructs along with examples.
- The benefits of leveraging this new construct in terms of scalability, flexibility, and simplicity.
By understanding the reasons behind its development and exploring its capabilities through practical examples, you will gain a comprehensive understanding of how this new L2 construct can enhance their DynamoDB experience. Let’s dive in.
Background
The original DynamoDB L2 Table construct is a powerful and versatile tool for creating and managing DynamoDB tables. It allows you to easily define the schema of your table, as well as the provisioned throughput and replicas. It also supports features like global tables, secondary indexes, and streams.
However, the Table construct uses a custom resource to add replicas to the primary table. This means that a separate Lambda function is created as the resource provider in addition to the Table resources (primary table and any replicas). This can be cumbersome to manage and can lead to drift detection issues.
The new TableV2 construct is an abstraction built on top of the GlobalTable L1 construct. It uses the CloudFormation resource AWS::DynamoDB::GlobalTable to create and manage DynamoDB tables. This has two important benefits:
- CloudFormation is in control and aware of all replicas that make up the Global Table, which means you will experience drift detection across all the replicas. With the original table construct, CloudFormation was not aware of any replicas since this was being handled through the Lambda function being used as a resource provider.
- No extra resource (Lambda function) is created when replicas are configured with TableV2. This eliminates the need to manage an extra resource and the risk of troubleshooting issues that may arise with the custom resource. TableV2 simplifies the setup and maintenance of DynamoDB tables by using native CloudFormation constructs to directly manage replicas, without the need for a Lambda function. This results in a more efficient and streamlined experience for users.
The new TableV2 construct provides more fine-grained control to customers over the replicas created as part of the Global Table. Specifically, customers can specify properties like contributor insights, deletion protection, point-in-time recovery, table class, read capacity, and global secondary index options on a per-replica basis.
This means that customers can tailor their table setup to meet their specific needs and optimize their overall experience with the Global Table feature. For example, a customer might want to enable contributor insights for all replicas, but only enable deletion protection for the primary replica. Or, a customer might want to use a different table class for each replica, depending on the expected workload.
The new TableV2 construct also offers greater flexibility and customization options by allowing customers to specify these properties on a per-replica basis. This can be helpful for customers who need to have different configurations for their replicas, or who want to fine-tune the performance and availability of their tables.
In the next section, we will explore each of these properties in more detail and how they can be specified in the new construct.
Features Walk-through
The new TableV2 construct is the recommended CDK DynamoDB construct for creating both single tables and global tables. In this section, we will review some specific aspects of the TableV2 construct and how they can be implemented. The walkthrough will cover features like Replicas, Billing, and Encryption, providing a comprehensive understanding of its capabilities.
Replicas
One of the most important benefits of the new L2 construct is the ability to configure properties on a per-replica basis. For example, the following code creates a global DynamoDB table with contributor insights and point-in-time recovery enabled for the table:
import * as cdk from 'aws-cdk-lib';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; const app = new cdk.App();
const stack = new cdk.Stack(app, 'Stack', { env: { region: 'us-west-2' } }); const globalTable = new dynamodb.TableV2(stack, 'GlobalTable', { partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING }, contributorInsights: true, pointInTimeRecovery: true, replicas: [ { region: 'us-east-1', tableClass: dynamodb.TableClass.STANDARD_INFREQUENT_ACCESS, pointInTimeRecovery: false, }, { region: 'us-east-2', contributorInsights: false, }, ],
}); // This is an ITableV2 instance for the replica table in us-east-1
const replica = globalTable.replica('us-east-1');
This code creates two replicas, one in the us-east-1 region and one in the us-east-2 region. For the replica in the us-east-1 region, we disable point-in-time recovery and set the table class to STANDARD_INFREQUENT_ACCESS. For the replica in the us-east-2 region, we disable contributor insights. The TableV2 construct also enables users to work with individual instances of the replicas in a global table via the replica() method. We see how this can be utilized from the above code where an ITableV2 instance representing the replica in us-east-1 is returned.
This is particularly useful for the grant() and metric() methods. For example, the following code gives a user write access to a replica in us-east-1 region:
import { Construct } from 'constructs';
import { App, Stack, StackProps } from 'aws-cdk-lib';
import { ITableV2, TableV2 } from 'aws-cdk-lib/aws-dynamodb';
import { AttributeType } from 'aws-cdk-lib/aws-dynamodb';
import * as iam from 'aws-cdk-lib/aws-iam'; class FooStack extends Stack { public readonly globalTable: TableV2; public constructor(scope: Construct, id: string, props: StackProps) { super(scope, id, props); this.globalTable = new TableV2(this, 'GlobalTable', { partitionKey: { name: 'pk', type: AttributeType.STRING }, replicas: [ { region: 'us-east-1' }, { region: 'us-east-2' }, ], }); }
} interface BarStackProps extends StackProps { readonly replicaTable: ITableV2;
} class BarStack extends Stack { public constructor(scope: Construct, id: string, props: BarStackProps) { super(scope, id, props); const user = new iam.User(this, 'User') // user is given grantWriteData permissions to replica in us-east-1 props.replicaTable.grantWriteData(user); }
} const app = new App(); const fooStack = new FooStack(app, 'FooStack', { env: { region: 'us-west-2', account: process.env.CDK_DEFAULT_ACCOUNT } });
const barStack = new BarStack(app, 'BarStack', { replicaTable: fooStack.globalTable.replica('us-east-1'), env: { region: 'us-east-1', account: process.env.CDK_DEFAULT_ACCOUNT },
});
Before the replica() method was introduced, grant methods on the original Table construct applied to the primary table and all replicas. This was because there was no way to pull out a specific replica. This limited a user’s ability to grant a specific principal read, write, or read/write permission to a specific replica. The replica() method enables granting specific permissions to individual replicas in a global table. It maintains consistent behavior across all methods in the ITableV2 interface, including grants and metrics.
Billing
Table billing is easily configured using the onDemand() or provisioned() static methods of the Billing class. If provisioned billing is configured, the user must provide read and write capacity, which can be easily configured using the fixed() or autoscaled() static methods of the Capacity class.
For example, to configure on-demand billing:
import * as cdk from 'aws-cdk-lib';
import { AttributeType, Billing, TableClass, TableV2 } from 'aws-cdk-lib/aws-dynamodb';
import { Construct } from 'constructs'; export class DynamodbStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); new TableV2(this, 'DynamoDBTable', { partitionKey: { name: 'id', type: AttributeType.STRING}, replicas: [ {region: 'us-east-2'}, {region: 'us-west-1'} ], billing: Billing.onDemand(), tableClass: TableClass.STANDARD }) }
}
To configure provisioned billing:
import * as cdk from 'aws-cdk-lib';
import { AttributeType, Billing, Capacity, TableClass, TableV2 } from 'aws-cdk-lib/aws-dynamodb';
import { Construct } from 'constructs'; export class DynamodbStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); new TableV2(this, 'DynamoDBTable', { partitionKey: { name: 'id', type: AttributeType.STRING}, replicas: [ {region: 'us-east-2'}, {region: 'us-west-1'} ], billing: Billing.provisioned({ readCapacity: Capacity.fixed(5), writeCapacity: Capacity.autoscaled({maxCapacity: 10}) }), tableClass: TableClass.STANDARD }) }
}
Note that with the previous Table construct, users had to set a billingMode property and configure readCapacity and writeCapacity as separate properties. Additionally, configuring autoscaled capacity required calling the autoScaleReadCapacity() or autoScaleWriteCapacity() method on an instance of the Table construct. Lastly, since readCapacity, writeCapacity, and billingMode were all individual properties, a user had to know not to provision read and write capacity for a table with PAY_PER_REQUEST billing mode. With the new Billing class, the user is guided into providing necessary properties via the onDemand() and provisioned() static methods.
Encryption
The TableEncryptionV2 class allows you to provide your own KMS keys for each replica instead of using the default AWS owned keys, thus encrypting every replica with a custom KMS key. This provides more granular control over the encryption of your DynamoDB tables.
Here is an example of how to use the TableEncryptionV2 class to encrypt each replica of a global table with a custom KMS key:
import * as cdk from 'aws-cdk-lib';
import { AttributeType, Billing, BillingMode, Capacity, TableBaseV2, TableEncryptionV2, TableV2 } from 'aws-cdk-lib/aws-dynamodb';
import { IKey, Key } from 'aws-cdk-lib/aws-kms';
import { Construct } from 'constructs'; interface KMSkeys extends cdk.StackProps { kmsuswest1: IKey; kmsuseast2: IKey;
} export class GlobalTableStack extends cdk.Stack { //public readonly globalTable: TableV2; constructor(scope: Construct, id: string, props: KMSkeys) { super(scope, id, props); const replicaTableKeys = { "us-west-1": props.kmsuswest1.keyArn, "us-east-2": props.kmsuseast2.keyArn } const TableKMSKey=new Key(this, 'TableKMSKey', { alias: 'KMSuswest2Stack', } ) new TableV2(this, 'GlobalTable', { tableName: 'FooTableFour', encryption: TableEncryptionV2.customerManagedKey(TableKMSKey,replicaTableKeys), partitionKey: { name: 'FooHashKey', type: AttributeType.STRING, }, replicas: [ { region: 'us-west-1', }, { region: 'us-east-2', }, ], }) }
}
The ability to provide custom KMS keys for each replica can help to improve the security of your DynamoDB tables. It also gives you more control over the encryption of your data. This can help you to meet specific compliance requirements.
Conclusion
In this post, I introduced the new AWS CDK TableV2 construct, highlighting its advantages over the original construct. Notably, TableV2 enables drift detection for replica tables and eliminates the need for an extra Lambda function custom resource. I delved into practical implementations, focusing on three key aspects: Replicas, Billing, and Encryption.
To summarize, TableV2 marks a substantial improvement over the original construct. Its user experience provides significant improvement over the original construct in several ways, such as:
- Direct support for global tables: TableV2 makes it easy to create and manage global DynamoDB tables.
- Easier configuration of global secondary indexes and Autoscaling: TableV2 provides a simplified and streamlined process for configuring global secondary indexes and Autoscaling.
- More granular control over replicas: TableV2 allows you to configure properties on a per-replica basis, giving you more control over the performance and availability of your tables.
- Improved API design and user experience: TableV2 improves the API design and user experience by implementing new classes for billing, capacity, and encryption.
Overall, TableV2 is a powerful and flexible construct that makes it easier to build and manage DynamoDB tables at scale. It is the preferred CDK DynamoDB construct for creating both single tables and global tables. If you are looking for a powerful and flexible way to build and manage DynamoDB tables, TableV2 is the perfect choice for you.
If you’re new to CDK and eager to get started, we highly recommend checking out the CDK documentation and the CDK workshop.