---
title: "Getting a local DynamoDB instance working"
slug: /local-dynamodb-setup-sam/
date: 2024-06-08
tags: ["log", "aws", "dynamodb"]
---

I wasted most of an afternoon trying to get the following set-up:

- Two local instances of DynamoDB running in Docker
- The ability to add table data to either instance via NoSQL Workbench and query
  it via an AWS lambda

If you are coming from SQLPro or DBeaver, Amazon's NoSQL workbench is a bit
unintuitive. Creating the lambdas and the Docker containers was easy, it was
getting them recognised by this client that was tricky. Anyway this is the
process.

I want to set up two Docker instances of the `amazon/dynamodb-local` image,
along with a bridging network so that the Docker container that SAM runs in can
communicate with the containers that DynamoDB runs in. Hence `docker-compose` is
the way to go:

```yml
# docker-compose.yaml

version: "3.8"
services:
  dev:
    image: amazon/dynamodb-local
    container_name: timetracking_dynamodb_dev
    ports:
      - "8000:8000"
    volumes:
      - "./data:/home/dynamodblocal/data"
    networks:
      - sam-local
  stage:
    image: amazon/dynamodb-local
    container_name: timetracking_dynamodb_stage
    ports:
      - "8001:8001"
    volumes:
      - "./data:/home/dynamodblocal/data"
    networks:
      - sam-local

networks:
  sam-local:
    driver: bridge
```

My two DDB instances are `dev` and `stage` running on ports 8000 and 8001
respectively. Their shared network is called `sam local`.

After running `docker-compose up`. I run `docker network ls` and it confirms the
network has been created (it prepends the network name with the repo name):

![](./img/docker-network-ls.png)

Next comes SAM. I've just created the default Typescript Lambda template using
the SAM CLI. For now this will only expose a single API Gateway endpoint,
`/fetch`, which I will use to GET my DynamoDB table data. The key info from the
template:

```yml
# template.yaml

Resources:
  TimeTrackingFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/
      Handler: index.handler
      Runtime: nodejs20.x
      Architectures:
        - x86_64
      Events:
        Fetch:
          Type: Api
          Properties:
            Path: /fetch
            Method: get
```

I'll use the following command to start the local APIGateway server:

```sh
sam build && sam local start-api --docker-network time-tracking_sam-local
```

If I run `docker network inspect time-tracking_sam-local`, I can confirm that
the SAM Docker instance (which AWS creates and which is effectively a black box
to me) is on the same network as the two DDB instances that I set up earlier:

![](./img/docker-network-inspect.png)

Next I want to view my containers in NoSQL workbench so I can create the table
schema and start adding data. I'm just going to worry about the `dev` container
here. I go to the _Operation builder_ and add a new connection:

![](./img/workbench-create-conn.png)

This automatically generates an _Access key ID_ and _Secret access key_ even
though this is a local service that won't interact with the production version
in any way. You _must_ use these. Some guides say you can put anything when
connecting locally but in my experience this is _not true_.

Using these keys, I write a skeletal lambda just to check the connection via the
AWS SDK for DynamoDB:

```ts
import { DynamoDBClient, ListTablesCommand } from "@aws-sdk/client-dynamodb"

const client = new DynamoDBClient({
   region: "localhost",
   endpoint: "http://dev:8000",
   credentials: {
      accessKeyId: "xxxx",
      secretAccessKey: "xxxx",
   },
})

export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
   try {
      const command = new ListTablesCommand({})
      const response = await client.send(command)
      return {
         statusCode: 200,
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify({
            message: "Successfully retrieved tables",
            tableNames: response.TableNames,
         }),
      }
...
```

The first problem because I wasn't providing a region. Why would I, this is
local? But it needs to be `localhost`. And again, you must put the actual
credential values that the Workbench gives you.

The next issue was that this function was returning an empty array when I
queried the `/fetch` endpoint. Even though I had set up a table in Workbench:

![](./img/workbench-data-modeller.png)

You must click _Commit to Amazon DynamoDB_ even though this is counter-intuitive
and suggests you are going to auto-provision paid services! You can then select
the local instance that you are going to seed with the table from the model:

![](./img/commit-table-workbench.png)

Now if you back to _Operation builder_, the table is associated with the given
instance and the data is displayed:

![](./img/ddb-data-in-builder.png)

On reflection, this makes a lot of sense as it decouples the data model from any
particular instance (local or production) that uses the data and makes it easy
to seed different tables to different database instances.

So finally, if I now query the `fetch` endpoint, I get the table name returned,
demonstrating that all the different parts have joined up:

![](./img/httpie-showing-data.png)
