Connecting Amplify AppSync to an imported DynamoDB Table

Amplify+AppSync with a Custom DynamoDB Resource

Amplify makes building apps easy, annotate your GraphQL schema with @model and Amplify will happily generate you a DynamoDB table you can Query/Mutate/Subscribe too…

What about when you have a DynamoDB Table created outside of Amplify? Maybe through CDK or CloudFormation (or just the console)? Or in other words, how do you Bring Your Own Database to Amplify?

The good news is, Amplify has you covered with Custom Resources, in this post, I’m going to walk you through it.

Firstly, have a quick read of Overwrite & customize resolvers in the Amplify docs, it’s almost there but not quite comprehensive enough for this particular use case.

OK, ready to go?

We’re going to hook up a DynamoDB table holding information about Lego sets and create a query so that we can fetch a set by its name.

This is the query in DynamoDB that we want to replicate in AppSync:

DynamoDB index query

First, import your DynamoDB Table via the Amplify CLI

amplify import storage

You’ll be asked a few questions, make sure you select “DynamoDB table” to import and the right Table you want connected to Amplify.

(base) / amplify import storage? Please select from one of the below mentioned services: DynamoDB table - NoSQL Database✔ Only one DynamoDB Table (LegoSets) found and it was automatically selected.✅ DynamoDB Table 'LegoSets' was successfully imported.Next steps:- This resource can now be accessed from REST APIs (‘amplify add api’) and Functions (‘amplify add function’)

At this point, Amplify will be aware of your DynamoDB locally, next step is to do an Amplify push to make sure we’re synced.

amplify push

Next, we want to extend our GraphQL schema to support the schema of the Table we’ve just added.

My table has a global secondary index on the “name” column of the table, I’m going to create a query to fetch a set by its name.

type LegoSet {
id: ID!,
name: String
}
type LegoSets {
items: [LegoSet]
}
type Query {
getLegoSetByName(name: String!): LegoSets
}

You’ll notice we haven’t annotated these types with any directives, this is because we need to manually hook AppSync up to our GraphQL schema and direct it to DynamoDB. This means we need to create a Custom Resource inside Amplify, this is kind of like an escape hatch outside of Amplify into the wider AWS ecosystem for when Amplify can’t satisfy your current need natively (in this case, bringing our own database).

We’re going to create an AppSync Data Source, Resolver and an IAM Role that AppSync can assume to CRUD our DynamoDB Table.

In your Amplify project, head over to ./amplify/backend/api/<project_name>/resolvers.

We’re going to create 2 Velocity Template Files here which tell AppSync how to map the request and response in to and out of DynamoDB.

The general convention here is to name these files consistently with your GraphQL Queries.

In this example, I’m going to create a Query.legoSetByName.req.vtl and Query.legoSetByName.res.vtl.

Open your req.vtl file, we’re going to specify the expressions we want to use to query our table and which index we want to target when AppSync gets a request for this resolver.

## Query.legoSetByName.req.vtl **{
"version": "2017-02-28",
"operation": "Query",
"query": {
"expression": "#dynamo_name = :setName",
"expressionNames": {
"#dynamo_name": "name"
},
"expressionValues": {
":setName": {
"S": "$context.args.name"
}
}
},
"index": "nameIndex"
}

This should be fairly straightforward, I’m targeting the global secondary index “nameIndex” and querying the “name” partition key in this index with the parameter $context.args.name which is passed in by AppSync from the GraphQL query. Note that “name” from “$context.args.name” is the parameter to my GraphQL query “getLegoSetByName”.

Next open your res.vtl file, we’re going to specify how AppSync should respond from this resolver.

## Query.legoSetByName.res.vtl **$util.toJson($ctx.result)

Now we have our resolver mapping templates configured, we can hook them up to a new AppSync Resolver within a new AppSync Data Source.

Open ./amplify/backend/api/<project_name>/stacks/CustomResource.json.

In the Resources section of the JSON, we’re firstly going to add an AppSync Data Source:

"DynamoDBDataSource": {
"Type": "AWS::AppSync::DataSource",
"Properties": {
"ApiId": {
"Ref": "AppSyncApiId"
},
"Name": "LegoSetsDataset",
"Type": "AMAZON_DYNAMODB",
"ServiceRoleArn": {
"Fn::GetAtt": [
"DynamoDBDataSourceRole",
"Arn"
]
},
"DynamoDBConfig": {
"TableName": "LegoSets",
"AwsRegion": "eu-west-1"
}
}
},

This should be fairly self explanatory, you need to ensure that the TableName matches the table you imported into Amplify and that the region is correct. Also ensure that you’re happy with the name of the Data Source.

Next, we’re going to create that DynamoDBDataSourceRole. We’re going to give AppSync full access to our DynamoDB Table.

"DynamoDBDataSourceRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"RoleName": "DynamoDBDataSource",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "appsync.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"Policies": [
{
"PolicyName": "DynamoDBFullAccess",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:*"
],
"Resource": [
{
"Fn::Sub": [
"arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/LegoCatalogueStack-LegoCatalogue510EBFF1-1ORXD6W1LHTDR/index/nameIndex",
{
"env": {
"Ref": "env"
}
}
]
}
]
}
]
}
}
]
}
},

Make sure the Resource ARN matches your table and index.

Finally, let’s create the AppSync Resolver.

"QueryLegoSetByNameResolver": {
"Type": "AWS::AppSync::Resolver",
"Properties": {
"ApiId": {
"Ref": "AppSyncApiId"
},
"DataSourceName": "LegoSetDataset",
"TypeName": "Query",
"FieldName": "getLegoSetByName",
"RequestMappingTemplateS3Location": {
"Fn::Sub": [
"s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.legoSetById.req.vtl",
{
"S3DeploymentBucket": {
"Ref": "S3DeploymentBucket"
},
"S3DeploymentRootKey": {
"Ref": "S3DeploymentRootKey"
}
}
]
},
"ResponseMappingTemplateS3Location": {
"Fn::Sub": [
"s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.legoSetById.res.vtl",
{
"S3DeploymentBucket": {
"Ref": "S3DeploymentBucket"
},
"S3DeploymentRootKey": {
"Ref": "S3DeploymentRootKey"
}
}
]
}
}
}

Ensure that DataSourceName matches the name of the DataSource we created earlier, along with the FieldName we’re using to query and finally ensure the Request and Response mapping S3 locations match the names of your resolver mapping files we just created.

Let's get this deployed to Amplify and give our query a go.

amplify push

Head over to the AWS Console -> AppSync -> Your API -> Queries, write your query and see your data returned from your table!

If you’re using React.js, you’ll now be able to import this query in your components and run queries against it.

import { getLegoSetDetails, getLegoSetByName } from '../graphql/queries';...useEffect(() => {
const legoSet = await API.graphql(graphqlOperation(getLegoSetByName, { name: "The Upside Down"}));
setLegoSet(legoSet.data);}

That’s it! Happy hacking.

Technical Principal @ AND Digital