Back to home

PrerequisitesGetting startedWriting a single-table constructInstantiating the tableAdding the first global secondary indexAdding the second global secondary indexAdding the construct to our appDeploying to LocalStackTeardownSummaryResources and further reading
DynamoDB With LocalStack And The AWS CDK (Part 1) main image

DynamoDB With LocalStack And The AWS CDK (Part 1)

This post will use the AWS TypeScript CDK to deploy table to DynamoDB on LocalStack with single-table schema design.

The code will build off the work done in the first two articles of the "Working with the TypeScript AWS CDK" series.

The final code can be found here.

Prerequisites

  1. Basic familiarity with AWS CDK for TypeScript.
  2. Read the first two posts in the "Working with the TypeScript AWS CDK" series.
  3. Familiary with DynamoDB. This post will not be an overview of DynamoDB development.

Getting started

We will clone the working repo from the previous blog post as a starting point:

1 2 3 4 $ git clone https://github.com/okeeffed/using-the-aws-cdk-with-localstack-and-aws-cdk-local dynamodb-with-localstack-and-the-aws-cdk $ cd building-a-cdn-with-s3-cloudfront-and-the-aws-cdk $ npm i # if install fails, remove the lockfile and try again

At this stage, we will have the app in a basic working state.

In order to create the CDN stack, we will require the following AWS CDK libraries:

1 2 @aws-cdk/core @aws-cdk/aws-dynamodb

We can install these prior to doing any work:

1 npm i @aws-cdk/core @aws-cdk/aws-dynamodb

The repository that we cloned already has a upgrade script supplied. We can use this to ensure our CDK packages are at parity:

1 npm run upgrade

We are now at a stage to write our construct.

Writing a single-table construct

In our particular case, I am aiming to write a re-useable construct that has single-table schema design at the forefront as well as being inclusive of two secondary indexes GSI1 and GSI2.

Let's create a folder lib/construct-single-table-dynamo and add a barrel file and a file for the construct:

1 2 $ mkdir lib/construct-single-table-dynamo $ touch lib/construct-single-table-dynamo/construct-single-table-dynamo.ts lib/construct-single-table-dynamo/index.ts

We want our construct to do three things:

  1. Instantiating the table.
  2. Add a global secondary index GSI1.
  3. Add a global secondary index GSI2.

As much as possible, we want to invert control to the consumer of the construct while also locking in some best practices around single-table design.

As a starter, let's prepare our lib/construct-single-table-dynamo/construct-single-table-dynamo.ts with the following code:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import * as cdk from "@aws-cdk/core"; import * as dynamodb from "@aws-cdk/aws-dynamodb"; type SingleTableGlobalSecondaryIndex = Omit< dynamodb.GlobalSecondaryIndexProps, "indexName" | "partitionKey" | "sortKey" >; export interface ConstructSingleTableDynamoProps extends cdk.StackProps { tableProps: Omit<dynamodb.TableProps, "partitionKey" | "sortKey">; GSI1Props?: SingleTableGlobalSecondaryIndex; GSI2Props?: SingleTableGlobalSecondaryIndex; } export class ConstructSingleTableDynamo extends cdk.Construct { constructor( scope: cdk.Construct, id: string, props?: ConstructSingleTableDynamoProps ) { super(scope, id); // ... ready to add our code } }

This code will enable our construct to take three props:

  1. tableProps to extend our table without allow the user to alter the partitionKey or sortKey.
  2. Optional GSI1Props to extend the first global secondary index without being able to alter the indexName, partitionKey or sortKey.
  3. Optional GSI2Props to extend the second global secondary index without being able to alter the indexName, partitionKey or sortKey.

We will provide sensible defaults here. You are free to alter those props if you want full control after setting sensible defaults!

Let's now go through and add our table and global secondary indexes.

Instantiating the table

We want to invoke dynamodb.Table to create an instance of a table and pass sensible props for that table.

We will also assign that instance to a publicly available accessor dynamoTable so that the consumer has access to the table when creating our construct within a stack.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import * as cdk from "@aws-cdk/core"; import * as dynamodb from "@aws-cdk/aws-dynamodb"; type SingleTableGlobalSecondaryIndex = Omit< dynamodb.GlobalSecondaryIndexProps, "indexName" | "partitionKey" | "sortKey" >; export interface ConstructSingleTableDynamoProps extends cdk.StackProps { tableProps: Omit<dynamodb.TableProps, "partitionKey" | "sortKey">; GSI1Props?: SingleTableGlobalSecondaryIndex; GSI2Props?: SingleTableGlobalSecondaryIndex; } export class ConstructSingleTableDynamo extends cdk.Construct { // code added here public readonly dynamoTable: dynamodb.Table; constructor( scope: cdk.Construct, id: string, props?: ConstructSingleTableDynamoProps ) { super(scope, id); // added code this.dynamoTable = new dynamodb.Table(this, `SingleTable`, { partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING, }, sortKey: { name: "sk", type: dynamodb.AttributeType.STRING, }, readCapacity: 1, writeCapacity: 1, ...props?.tableProps, }); } }

Adding the first global secondary index

We can use the addGlobalSecondaryIndex method on an instance of dynamodb.Table to create a global secondary index.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import * as cdk from "@aws-cdk/core"; import * as dynamodb from "@aws-cdk/aws-dynamodb"; type SingleTableGlobalSecondaryIndex = Omit< dynamodb.GlobalSecondaryIndexProps, "indexName" | "partitionKey" | "sortKey" >; export interface ConstructSingleTableDynamoProps extends cdk.StackProps { tableProps: Omit<dynamodb.TableProps, "partitionKey" | "sortKey">; GSI1Props?: SingleTableGlobalSecondaryIndex; GSI2Props?: SingleTableGlobalSecondaryIndex; } export class ConstructSingleTableDynamo extends cdk.Construct { public readonly dynamoTable: dynamodb.Table; constructor( scope: cdk.Construct, id: string, props?: ConstructSingleTableDynamoProps ) { super(scope, id); this.dynamoTable = new dynamodb.Table(this, `SingleTable`, { partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING, }, sortKey: { name: "sk", type: dynamodb.AttributeType.STRING, }, readCapacity: 1, writeCapacity: 1, ...props?.tableProps, }); // added code this.dynamoTable.addGlobalSecondaryIndex({ indexName: "GSI1", partitionKey: { name: "GSI1PK", type: dynamodb.AttributeType.STRING, }, sortKey: { name: "GSI1SK", type: dynamodb.AttributeType.STRING, }, readCapacity: 1, writeCapacity: 1, ...props?.GSI1Props, }); } }

Adding the second global secondary index

We can repeat similar to the above for the second index.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import * as cdk from "@aws-cdk/core"; import * as dynamodb from "@aws-cdk/aws-dynamodb"; type SingleTableGlobalSecondaryIndex = Omit< dynamodb.GlobalSecondaryIndexProps, "indexName" | "partitionKey" | "sortKey" >; export interface ConstructSingleTableDynamoProps extends cdk.StackProps { tableProps: Omit<dynamodb.TableProps, "partitionKey" | "sortKey">; GSI1Props?: SingleTableGlobalSecondaryIndex; GSI2Props?: SingleTableGlobalSecondaryIndex; } export class ConstructSingleTableDynamo extends cdk.Construct { public readonly dynamoTable: dynamodb.Table; constructor( scope: cdk.Construct, id: string, props?: ConstructSingleTableDynamoProps ) { super(scope, id); this.dynamoTable = new dynamodb.Table(this, `SingleTable`, { partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING, }, sortKey: { name: "sk", type: dynamodb.AttributeType.STRING, }, readCapacity: 1, writeCapacity: 1, ...props?.tableProps, }); this.dynamoTable.addGlobalSecondaryIndex({ indexName: "GSI1", partitionKey: { name: "GSI1PK", type: dynamodb.AttributeType.STRING, }, sortKey: { name: "GSI1SK", type: dynamodb.AttributeType.STRING, }, readCapacity: 1, writeCapacity: 1, ...props?.GSI1Props, }); // added code this.dynamoTable.addGlobalSecondaryIndex({ indexName: "GSI2", partitionKey: { name: "GSI2PK", type: dynamodb.AttributeType.STRING, }, sortKey: { name: "GSI2SK", type: dynamodb.AttributeType.STRING, }, readCapacity: 1, writeCapacity: 1, ...props?.GSI2Props, }); } }

At this stage, we are now able to begin creating DynamoDB constructs in our AWS CDK stacks.

Adding the construct to our app

The repository that we cloned already creates an app with stack AwsCdkWithTypescriptFoundationsStack.

We can see this happen in the file bin/aws-cdk-with-typescript-foundations.ts.

What we can do then is to edit that stack code to create an instance of our constructed DynamoDB table.

Update the file lib/aws-cdk-with-typescript-foundations-stack.ts to have the following code:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import * as cdk from "@aws-cdk/core"; import { ConstructSingleTableDynamo } from "./construct-single-table-dynamo"; export class AwsCdkWithTypescriptFoundationsStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // The code that defines your stack goes here new ConstructSingleTableDynamo(this, "MyFirstTable", { tableProps: { tableName: "my-first-table", }, }); } }

This stack will construct our DynamoDB table named my-first-table.

At this stage, we are ready to synthesize and deploy our stack to LocalStack.

Deploying to LocalStack

The docker-compose.yml file already has what is required for us to startup LocalStack.

Run docker compose up for LocalStack to start on Docker.

Once that is started, we can go through our usual lifecycle of the AWS CDK by synthesizing and deploying our app.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 $ npm run local synth > aws-cdk-with-typescript-foundations@0.1.0 cdk > cdk "synth" Resources: MyFirstTableSingleTable60CA70C8: Type: AWS::DynamoDB::Table Properties: KeySchema: - AttributeName: pk KeyType: HASH - AttributeName: sk KeyType: RANGE AttributeDefinitions: - AttributeName: pk AttributeType: S - AttributeName: sk AttributeType: S - AttributeName: GSI1PK AttributeType: S - AttributeName: GSI1SK AttributeType: S - AttributeName: GSI2PK AttributeType: S - AttributeName: GSI2SK AttributeType: S GlobalSecondaryIndexes: - IndexName: GSI1 KeySchema: - AttributeName: GSI1PK KeyType: HASH - AttributeName: GSI1SK KeyType: RANGE Projection: ProjectionType: ALL ProvisionedThroughput: ReadCapacityUnits: 1 WriteCapacityUnits: 1 - IndexName: GSI2 KeySchema: - AttributeName: GSI2PK KeyType: HASH - AttributeName: GSI2SK KeyType: RANGE Projection: ProjectionType: ALL ProvisionedThroughput: ReadCapacityUnits: 1 WriteCapacityUnits: 1 ProvisionedThroughput: ReadCapacityUnits: 1 WriteCapacityUnits: 1 TableName: my-first-table UpdateReplacePolicy: Retain DeletionPolicy: Retain Metadata: aws:cdk:path: AwsCdkWithTypescriptFoundationsStack/MyFirstTable/SingleTable/Resource CDKMetadata: Type: AWS::CDK::Metadata Properties: Analytics: v2:deflate64:H4sIAAAAAAAAE02NTQ6CMBBGz+J+GOyC6NKEG6AXKNMxKchM0h+MaXp3EDeuvrd4eZ9BYy54Pt3sOzbk5raQBsZyT5Zm6FViCpkSDBw1B2Lon/LPu+F88ioVvgX3EbuoG7E87Pg6jANqBVHHOMV2NR1e98cpet+ELMkvjMNvNw7k/m2OAAAA Metadata: aws:cdk:path: AwsCdkWithTypescriptFoundationsStack/CDKMetadata/Default Condition: CDKMetadataAvailable Conditions: CDKMetadataAvailable: Fn::Or: - Fn::Or: - Fn::Equals: - Ref: AWS::Region - af-south-1 - Fn::Equals: - Ref: AWS::Region - ap-east-1 - Fn::Equals: - Ref: AWS::Region - ap-northeast-1 - Fn::Equals: - Ref: AWS::Region - ap-northeast-2 - Fn::Equals: - Ref: AWS::Region - ap-south-1 - Fn::Equals: - Ref: AWS::Region - ap-southeast-1 - Fn::Equals: - Ref: AWS::Region - ap-southeast-2 - Fn::Equals: - Ref: AWS::Region - ca-central-1 - Fn::Equals: - Ref: AWS::Region - cn-north-1 - Fn::Equals: - Ref: AWS::Region - cn-northwest-1 - Fn::Or: - Fn::Equals: - Ref: AWS::Region - eu-central-1 - Fn::Equals: - Ref: AWS::Region - eu-north-1 - Fn::Equals: - Ref: AWS::Region - eu-south-1 - Fn::Equals: - Ref: AWS::Region - eu-west-1 - Fn::Equals: - Ref: AWS::Region - eu-west-2 - Fn::Equals: - Ref: AWS::Region - eu-west-3 - Fn::Equals: - Ref: AWS::Region - me-south-1 - Fn::Equals: - Ref: AWS::Region - sa-east-1 - Fn::Equals: - Ref: AWS::Region - us-east-1 - Fn::Equals: - Ref: AWS::Region - us-east-2 - Fn::Or: - Fn::Equals: - Ref: AWS::Region - us-west-1 - Fn::Equals: - Ref: AWS::Region - us-west-2 $ npm run local deploy > aws-cdk-with-typescript-foundations@0.1.0 local > cdklocal "deploy" AwsCdkWithTypescriptFoundationsStack: deploying... AwsCdkWithTypescriptFoundationsStack: creating CloudFormation changeset... ✅ AwsCdkWithTypescriptFoundationsStack Stack ARN: arn:aws:cloudformation:us-east-1:000000000000:stack/AwsCdkWithTypescriptFoundationsStack/1653ab7b

At this stage we can check to see our table exists with the awslocal package (or aws with the LocalStack endpoint url http://localhost:4566):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 $ awslocal dynamodb describe-table --table-name my-first-table { "Table": { "AttributeDefinitions": [ { "AttributeName": "pk", "AttributeType": "S" }, { "AttributeName": "sk", "AttributeType": "S" }, { "AttributeName": "GSI1PK", "AttributeType": "S" }, { "AttributeName": "GSI1SK", "AttributeType": "S" }, { "AttributeName": "GSI2PK", "AttributeType": "S" }, { "AttributeName": "GSI2SK", "AttributeType": "S" } ], "TableName": "my-first-table", "KeySchema": [ { "AttributeName": "pk", "KeyType": "HASH" }, { "AttributeName": "sk", "KeyType": "RANGE" } ], "TableStatus": "ACTIVE", "CreationDateTime": 1628495490.523, "ProvisionedThroughput": { "LastIncreaseDateTime": 0.0, "LastDecreaseDateTime": 0.0, "NumberOfDecreasesToday": 0, "ReadCapacityUnits": 1, "WriteCapacityUnits": 1 }, "TableSizeBytes": 0, "ItemCount": 0, "TableArn": "arn:aws:dynamodb:us-east-1:000000000000:table/my-first-table", "GlobalSecondaryIndexes": [ { "IndexName": "GSI2", "KeySchema": [ { "AttributeName": "GSI2PK", "KeyType": "HASH" }, { "AttributeName": "GSI2SK", "KeyType": "RANGE" } ], "Projection": { "ProjectionType": "ALL" }, "IndexStatus": "ACTIVE", "ProvisionedThroughput": { "ReadCapacityUnits": 1, "WriteCapacityUnits": 1 }, "IndexSizeBytes": 0, "ItemCount": 0, "IndexArn": "arn:aws:dynamodb:ddblocal:000000000000:table/my-first-table/index/GSI2" }, { "IndexName": "GSI1", "KeySchema": [ { "AttributeName": "GSI1PK", "KeyType": "HASH" }, { "AttributeName": "GSI1SK", "KeyType": "RANGE" } ], "Projection": { "ProjectionType": "ALL" }, "IndexStatus": "ACTIVE", "ProvisionedThroughput": { "ReadCapacityUnits": 1, "WriteCapacityUnits": 1 }, "IndexSizeBytes": 0, "ItemCount": 0, "IndexArn": "arn:aws:dynamodb:ddblocal:000000000000:table/my-first-table/index/GSI1" } ] } }

Perfect! We have successfully constructed our DynamoDB table onto LocalStack.

Teardown

As always, we will also teardown the stack:

1 2 3 4 5 6 7 8 9 npm run local destroy > aws-cdk-with-typescript-foundations@0.1.0 local > cdklocal "destroy" Are you sure you want to delete: AwsCdkWithTypescriptFoundationsStack (y/n)? y AwsCdkWithTypescriptFoundationsStack: destroying... ✅ AwsCdkWithTypescriptFoundationsStack: destroyed

Summary

Today's post demonstrated how to create a single-table design DynamoDB table using the AWS TypeScript CDK. It did this by first creating a reusable construct, then followed it up by demonstrating usage of it in an app by deploying to LocalStack.

Tomorrow's post (as part two) will demonstrate this table in action with DynamoDB Toolbox.

Resources and further reading

Photo credit: benwksi

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on workingoutloud.dev, Den Dribbles and LandPad .

Related articles


1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.