# DynamoDB With LocalStack And The AWS CDK (Part 1)

> A walkthrough on how to deploy a local DynamoDB table to LocalStack with the AWS TypeScript CDK.

- Canonical: https://dennisokeeffe.com/blog/2021-08-09-dynamodb-with-localstack-and-the-aws-cdk
- Published: 2021-08-09
- Updated: 2021-08-09
- Tags: aws, aws-cdk, typescript

> This post is #4 of a multi-part series on the [AWS CDK with TypeScript](https://blog.dennisokeeffe.com/blog/series/working-with-the-type-script-aws-cdk) written during Melbourne lockdown #6.

This post will use the [AWS TypeScript CDK](https://docs.aws.amazon.com/cdk/latest/guide/work-with-cdk-typescript.html) 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"](https://blog.dennisokeeffe.com/blog/series/working-with-the-type-script-aws-cdk) series.

The final code can be found [here](https://github.com/okeeffed/dynamodb-with-localstack-and-the-aws-cdk).

## Prerequisites

1. Basic familiarity with [AWS CDK for TypeScript](https://docs.aws.amazon.com/cdk/latest/guide/work-with-cdk-typescript.html).
2. Read the first two posts in the ["Working with the TypeScript AWS CDK"](https://blog.dennisokeeffe.com/blog/series/working-with-the-type-script-aws-cdk) series.
   - [AWS CDK With TypeScript Foundations](https://blog.dennisokeeffe.com/blog/2021-08-06-aws-cdk-with-typescript-foundations).
   - [Using The AWS CDK With LocalStack And `aws-cdk-local`](https://blog.dennisokeeffe.com/blog/2021-08-07-using-the-aws-cdk-with-localstack-and-aws-cdk-local).
3. Familiary with [DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html). 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:

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

```s
@aws-cdk/core
@aws-cdk/aws-dynamodb
```

We can install these prior to doing any work:

```s
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:

```s
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:

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

```ts
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.

```ts
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.

```ts
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.

```ts
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:

```ts
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.

```s
$ 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`](https://github.com/localstack/awscli-local) (or `aws` with the LocalStack endpoint url `http://localhost:4566`):

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

```s
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](https://github.com/jeremydaly/dynamodb-toolbox).

## Resources and further reading

1. [AWS CDK for TypeScript](https://docs.aws.amazon.com/cdk/latest/guide/work-with-cdk-typescript.html)
2. [Final code](https://github.com/okeeffed/dynamodb-with-localstack-and-the-aws-cdk)
3. [`awslocal package`](https://github.com/localstack/awscli-local)
4. ["Working with the TypeScript AWS CDK" series](https://blog.dennisokeeffe.com/blog/series/working-with-the-type-script-aws-cdk)
5. [AWS CDK With TypeScript Foundations](https://blog.dennisokeeffe.com/blog/2021-08-06-aws-cdk-with-typescript-foundations)
6. [Using The AWS CDK With LocalStack And `aws-cdk-local`](https://blog.dennisokeeffe.com/blog/2021-08-07-using-the-aws-cdk-with-localstack-and-aws-cdk-local)
7. [DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html)

_Photo credit: [`benwksi`](https://unsplash.com/@benwksi)_
