Checkout the branch workshop/02-hello-dynamo
.
Use this command to deploy the example from the deploy
folder:
npm run cdk -- deploy DeployLambdaStack --profile personal
This assumes you've deployed the Lambda in the previous step. If you didn't, you'll need to pass the same --parameters DeployLambdaStack:DomainName=hello
argument to set the parameter.
This will process any updates to the API Gateway and Lambda, then add the DynamoDB table and appropriate permissions.
DynamoDB is a NoSQL database hosted by AWS and secured using AWS IAM. It has some quirks in how you do table design (but that's a completely separate topic and you can start on Twitter here: @dynamodb where there are a LOT of examples and links to webinars about how to design tables for DynamoDB).
In this case, we're going to extend the simple example (which stores and retrieves values) to store the values in DynamoDB and fetch them.
The API has a few calls:
GET /api/values
: get all the valuesPOST /api/values
: add a new value to the next IDPUT /api/values/{id}
: replace the specified ID (or create it)GET /api/values/{id}
: get the specified ID (if it exists)DELETE /api/values/{id}
: delete the specified ID
The table looks something like this:
- Main
- Hash Key:
id
(example:value|123
): This is a concatenation so we can store different item types in the same table. Otherwise it's the ID of the value. - Attribute:
value
(example:The String Value
): The value that is set for the ID. - Attribute:
dummy
(example:1
): A value that is used to trick DynamoDB into sorting IDs when we get the secondary index.
- Hash Key:
- Secondary Index:
SortedIndex
- Hash Key:
dummy
(same value for all items) - Range Key:
id
(sorted lexographically)
- Hash Key:
- Other Special Considerations
- It's a little weird finding the maximum ID (and it is extremely error prone). We handle this by storing a special ID item in the table:
latest|0
that we examine and replace with an incremented index. The latest ID will always be in an item with the Hash Key:latest|0
and thevalue
will be the ID of the latest item. This means that when we do aPOST /api/values
, we'll calculate the latest item and do a batch write to write both changes at once. This will not scale, of course, and if enough people hit the API at once, it will create collisions. I will leave resolving that as an exercise to the inquisitive reader (and also urge people not to create an API with strictly incrementing integral IDs when they know that DynamoDB is in the background.- Also note: this is the most complex part of the
ValuesService
which is a hint that we're doing it wrong. If you follow good design principles that work WITH DynamoDB instead of against it, you'll find that your interactions with DynamoDB are very short and sweet.
- Also note: this is the most complex part of the
- DynamoDB will find items by the Hash Key / Range Key combination and perform upserts automatically. So this is extremely simple code.
- It's a little weird finding the maximum ID (and it is extremely error prone). We handle this by storing a special ID item in the table:
Starting from the workshop/02-hello-dynamo
tag, let's see what's looking different.
There are some new settings that were added to the appsettings.json
file that get overridden in Development mode.
"DynamoDB": {
"TableNamePrefix": "HelloWorldWeb"
}
Similarly, appsettings.Development.json
has one more property:
"DynamoDB": {
"ServiceURL": "http://localhost:8000",
"TableNamePrefix": ""
}
ServiceURL
is used to tell the DynamoDB client to override the endpoint for DynamoDB. By default, the client will look for credentials in the IAM role of the running code as well as the AWS Region the code is running in to select the correct endpoint. Usually, you want to use the endpoint in your current region to read the database (this is faster and cheaper). There is a feature called DynamoDB Global Tables in case you're interested in building cross-region applications on DynamoDB. In this case, we've used a local URL that will point to the DynamoDB emulator running in a container.
To make testing easier, there's a Docker Compose project setup in test/hello-dynamo/docker-compose.yml
that can be spun up with:
docker compose up -d
This will start the local DynamoDB instance and use port 8000 on your local machine to accept connections. When DynamoDB is run locally, it still requires some credentials to be used for the commands, but it will accept any credentials.
During startup, the application will look for the overridden ServiceURL and pass the dummy credentials instead. Otherwise, it will use whatever credentials are part of the IAM Role for the running application in AWS.
AWS has a tool called NoSQL Workbench for DynamoDB that can be used to work with the local instance and try out queries and table designs. It has some example designs you can look at as well.
The .NET client for DynamoDB has three flavors of APIs you can use to interact with DynamoDB:
- Low Level API: this uses the DynamoDB API directly and gives you the most control over interacting with the tables and using all the built-in features of DynamoDB. If you have some complex table designs that require using some of the deeper features of DynamoDB, you'll need this mode.
- Document Model: this simplifies the DynamoDB API into a model that behaves similarly to a MongoDB driver or other C#-friendly Document database. This is likely the simplest API that doesn't require creating any special classes or attributes.
- Object Persistence Mode (DynamoDBContext): this requires you to define data transfer objects and decorate them with attributes, giving the context hints on how your queries will work. The DynamoDBContext will create the appropriate mappings to return strongly typed objects instead of generic documents of the low level attribute maps. If your data model is well-defined, this will be the easiest way to go and the least error prone. If your data model is in flux or if your items could have completely different attributes, then this mode will give you problems.
In any case, all the modes are available together, so you can mix and match as you need to. If you are using the DynamoDBContext, you'll need to be allowed to call the DescribeTable
API on the DynamoDB Table, so be sure it include that in your role policy.
For convenience, a Swagger UI page will be hosted on the API when running locally. This makes it much easier to work locally, and you can import the schema file into Postman if that's more your speed.
When the application is deployed to AWS, the Swagger document and UI pages are not created. This saves quite a bit of time on startup.
The test project was updated to create a test fixture that will create the necesarry table in a local DynamoDB to run the unit tests against a real database. These are handled by the test/AwsHelloWorldWeb.Tests/IntegrationFixture.cs
file and are created once per test collection. This will create the DynamoDB schema (deleting the old one) and let you run tests against the database.
To run the application and testing locally, you'll need to fire up the local DynamoDB instance. Navigate to the test/hello-dynamo
folder with your favorite terminal and run the following:
docker compose up -d
This will start the DynamoDB locally on port 8000. If you need a different port, you can edit the docker compose file to remap it. However, there's some other logic around http://localhost:8000
so you'll need to search and replace the instances of that in the code with your updated port number.
It will also create a data
folder where the PostgreSQL files are persisted so that the data is kept around between database creation and deletion. If you want to clear out the database, just stop the container:
docker compose down
And delete the folder.
You can start and stop the service using the Docker Desktop Dashboard.
We're still deploying at a Lambda + API Gateway, but also adding the DynamoDB table that's created. We will use the CDK Table
item to create the DynamoDB table and the Global Secondary Index we'll need to get a sorted list.
After the table is defined, we need to make sure the Lambda is allowed to query it, hence the:
table.Table.GrantFullAccess(lambda.Function);
Use this command to deploy the example from the deploy
folder:
npm run cdk -- deploy DeployLambdaStack --profile personal
This assumes you've deployed the Lambda in the previous step. If you didn't, you'll need to pass the same --parameters DeployLambdaStack:DomainName=hello
argument to set the parameter.
This will process any updates to the API Gateway and Lambda, then add the DynamoDB table and appropriate permissions.