With a set of PokemonCsv
structs ready to go, and an AWS account to upload them to, we need to get them into our database, DyanmoDB.
Amazon DynamoDB is
- fully managed, which means you never need to think about “instances” or memory requirements.
- “NoSQL”, which means you have to identify your access patterns when designing your data model rather than when you’re querying the data.
DynamoDB also pairs well with Serverless functions. It is capable of scaling alongside the number of functions that are running and offering single-digit millisecond performance the whole time.
The most straightforward way to think about working with DynamoDB is to think of it as a key/value store.
If we have a Pokemon datatype that has a name, some types, a height, and a weight the JSON would look like this.
{
"name": "Bulbasaur",
"typing": ["Grass", "Poison"],
"height": 7,
"weight": 69
}
In Dynamo, you could think about that data being represented as a JSON object with keys that are lowercased Pokemon names, and the values we get back as being the JSON shown above.
{
"bulbasaur": {
"name": "Bulbasaur",
"typing": ["Grass", "Poison"],
"height": 7,
"weight": 69
},
"charmeleon": {
"name": "Charmeleon",
"typing": ["Fire"],
"height": 11,
"weight": 190
}
}
We’ll cover more advanced indexing strategies as we need them.
To be able to put data into DynamoDB, we need a DynamoDB table to put data in to. For this we’ll use the CDK app we set up earlier.
Destructure aws_dynamodb
off of the aws-cdk-lib
import in lib/infra-stack.js
.
Then in the constructor
function, create a new aws_dynamodb.Table
. This function accepts the current context as it’s first argument, a name for the table as the second, and some configuration options as the third.
const { Stack, aws_dynamodb } = require("aws-cdk-lib");
class InfraStack extends Stack {
/**
*
* @param {Construct} scope
* @param {string} id
* @param {StackProps=} props
*/
constructor(scope, id, props) {
super(scope, id, props);
const pokemonTable = new aws_dynamodb.Table(this, "PokemonTable", {
billingMode: aws_dynamodb.BillingMode.PAY_PER_REQUEST,
partitionKey: {
name: "pk",
type: aws_dynamodb.AttributeType.STRING,
},
});
}
}
module.exports = { InfraStack };
In this case we’ve set the billingMode
to PAY_PER_REQUEST
. The other option here would’ve been PROVISIONED
.
PAY_PER_REQUEST
is a pay-for-what-you-use billing method, while PROVISIONED
can offer lower pricing for more predictable workloads. If you’re interested or concerned about how this pricing is going to affect you, you can read more on the AWS DynamoDB pricing page which includes more details about both billing modes as well as a pricing calculator.
As a general estimate, you can expect to pay about $10 for 10 million write requests to DynamoDB, so it’s unlikely that you have to pay more than a few cents if you’re working through this workshop.
The next configuration option is the partitionKey
. This is the “key” value we talked about earlier that was a lowercased Pokemon name. We have to give it a name, which by convention is usually something generic like pk
, and we also have to give it a type. In this case it’s a String.
We store the result of this table creation in a variable called pokemonTable
because we’re going to need it later when we deploy our lambda functions.
Deploying a DynamoDB Database
We can now use the version of CDK we have installed in our infra project to do a diff
on our infrastructure. In this case we once again use the profile we set up.
npm run cdk diff -- --profile rust-adventure-playground
A CDK diff will take a look at our CDK code, its generated CloudFormation, and our live infrastructure in AWS. Then it will output a number of different values that tell us what is going to change if we deploy.
npm run cdk diff -- --profile rust-adventure-playground
> infra@0.1.0 cdk
> cdk "diff" "--profile" "rust-adventure-playground"
Stack InfraStack
Parameters
[+] Parameter BootstrapVersion BootstrapVersion: {"Type":"AWS::SSM::Parameter::Value<String>","Default":"/cdk-bootstrap/hnb659fds/version","Description":"Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"}
Conditions
[+] Condition CDKMetadata/Condition 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"]}]}]}
Resources
[+] AWS::DynamoDB::Table PokemonTable PokemonTable7DFA0E9C
Other Changes
[+] Unknown Rules: {"CheckBootstrapVersion":{"Assertions":[{"Assert":{"Fn::Not":[{"Fn::Contains":[["1","2","3","4","5"],{"Ref":"BootstrapVersion"}]}]},"AssertDescription":"CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."}]}}
In this case
- the
Parameters
say that we’re going to fetch the bootstrapped CDK version, - the
Conditions
tell us that CDK will make sure we’re deploying in a valid region - the
Resources
show up that we’ll be creating a new DynamoDB table - the
Other Changes
tells us that CDK will asset that we have a compatible version of the CDK bootstrap where we’re trying to deploy
If you’re comfortable with this, you can then deploy
in the same way you ran the diff.
❯ npm run cdk deploy -- --profile rust-adventure-playground
> infra@0.1.0 cdk
> cdk "deploy" "--profile" "rust-adventure-playground"
✨ Synthesis time: 0.61s
InfraStack: deploying...
[0%] start: Publishing fb81327202773b78f6aaee311bc58d432467139831190ff3d374983d76d65882:current_account-current_region
[100%] success: Published fb81327202773b78f6aaee311bc58d432467139831190ff3d374983d76d65882:current_account-current_region
InfraStack: creating CloudFormation changeset...
✅ InfraStack
✨ Deployment time: 54.82s
Stack ARN:
arn:aws:cloudformation:us-east-1:951103832835:stack/InfraStack/7724deb0-b1fd-11ec-8186-0e41eb28ac6d
✨ Total time: 55.44s
If anything goes wrong for some reason, CDK will rollback the changes and you’ll be left in the same state you were in before you tried to deploy.
We can check that the table was created by using the AWS CLI with our profile to list-tables
that have been created using the dynamodb subcommand group.
❯ aws dynamodb --profile rust-adventure-playground list-tables
{
"TableNames": [
"InfraStack-PokemonTable7DFA0E9C-1II2IAD7OZ2EJ"
]
}
After deploying, you’ll see a new folder called cdk.out
. This is basically “compiled json files” so they have a place to be before being uploaded to AWS and can be added to your .gitignore.
Save the name of the Table we’ve created as we’ll need to use it to upload data in the next lesson.
For now we’ll use env::var
so we can set the table name via an environment variable.
use std::env;
#[tokio::main]
async fn main() -> eyre::Result<()> {
color_eyre::install()?;
let table_name = env::var("TABLE_NAME")?;
...
}