🎉 I'm releasing 12 products in 12 months! If you love product, checkout my new blog workingoutloud.dev

Back to home

DynamoDB With LocalStack And The AWS CDK (Part 1)

This post is #4 of a multi-part series on the AWS CDK with TypeScript written during Melbourne lockdown #6.

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:

$ 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:

@aws-cdk/core @aws-cdk/aws-dynamodb

We can install these prior to doing any work:

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:

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:

$ 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:

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.

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.

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.

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:

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.

$ 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):

$ 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:

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

Personal image

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 Visibuild.

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.

DynamoDB With LocalStack And The AWS CDK (Part 1)

Introduction

Share this post