Rapid prototyping with AWS AppSync and serverless

October 7 2018

Sometimes you need to be able to prototype an application or portion of one quickly and your data/access layer shouldn't get in the way. Fortunately, AWS AppSync makes this wholly simple.

Write your serverless configuration

You can potentially get away with just two packages:

npm install --save-dev serverless serverless-appsync-plugin

By now there are hundreds of articles on serverless' framework of the same name. You can also check out the documentation. Servleress ships with a CLI tool you can use to spin up your configurations from a number of templates to suit your environment. I'm not going to get into them, but we're going to be using Node 8.10. In many cases I've found that the templates are a little more robust that my needs and I tend prefer to keep it simple.

Also, repo for serverless-appsync-plugin.

service:
  name: rapid-prototyping

plugins:
  - serverless-appsync-plugin

provider:
  name: aws
  runtime: nodejs8.10
  stage: ${opt:stage, 'dev'}
  region: us-east-1

The provider definition is important. It's going to tell serverless you're using aws, node 8.10, and your default region is us-east-1. The stage setting here is using one of serverless' many interpolation functions. In this case opt:stage refers to a CLI argument of stage but it will default to dev. If you're familiar with using the AWS cli then you probably already have a profile setup. You can append the profile alias to the provider here too, otherwise, the default will be used.

Configure serverless-appsync-plugin

Next you'll want to add the beginnings of your AppSync configuration.

custom:
  accountId: # Your AWS account id
  appSync:
    name: ${self:provider.stage}-rapid-prototypes-ftw
    authenticationType: API_KEY

serverless-appsync-plugin is going to read these specific values as part of its lifecycle. Note that I've actually used another interpolation method to prepend the stage value to the actual deploy. In most quick cases this is fast enough for me. However, if you prefer IAM role based separation of deployables then that's great too!

AppSync uses DynamoDB's template mapping and resolution layer. Meaning that you can simply define mappings to your data and associate them with your schema.

Create your template mapping

Start by creating a directory in your project, serverless-appsync-plugin defaults to mapping-templates. Within that directory create two files: Cat.request and Cat.response. Obviously the extensions here are my convenience and in some cases a generic response file may be helpful to keep things simple.

I'm going to keep this example simple and say that the request only returns a single cat. So our Cat.request could look something like this:

// Cat.request
{
  "version": "2017-02-28",
  "operation": "GetItem",
  "key": {
    "id": { "S": "${context.arguments.id}" }
  }
}

Without going into great detail about DynamoDB's mapping templates, this will map the argument of id to the request and execute a DynamoDB Get on the record.

Our Cat.response file is relatively simple:

$util.toJson($context.result)

The request will return a json encoded instance of whatever Cat we attempted to Get.

Update your serverless config with these mappings

Now that we've a basic mapping template we can update our serverless configuration to use it.

custom:
  accountId: # Your AWS account id
  appSync:
    name: ${self:provider.stage}-rapid-prototypes-ftw
    authenticationType: API_KEY
    mappingTemplates:
      - dataSource: ${self:provider.stage}Cats
        type: Query
        field: cat # maps to the query name in your schema
        request: Cat.request
        response: Cat.response
    serviceRole: 'Dynamo-AppSyncServiceRole'
    dataSources:
      - type: AMAZON_DYNAMODB
        name: ${self:provider.stage}Cats
        description: 'Cats table'
        config:
          tableName: ${self:provider.stage}-Cats
          serviceRoleArn: 'arn:aws:iam::${self:custom.accountId}:role/Dynamo-AppSyncServiceRole'

This is everything that serverless-appsync-plugin needs to deploy your service. Note that in my situation the ARN for the role has already been created through the CLI or Console. You can define roles in the resources section of a serverless config and then reference them with Attribute functions.

Define your DynamoDB resource with serverless

One of the extremely useful things about rapid prototyping this way is that you can many different types of resources related to your service. In situations where you're done testing or you no longer need the service you can destroy the entire deployment in one go. This is especially useful for managing your DynamoDB tables.

resources:
  Resources:
    CatsTable:
      Type: 'AWS::DynamoDB::Table'
      Properties:
        TableName: ${self:provider.stage}-Cats
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

This last portion of our configuration will instruct serverless to create a new DynamoDB table <stage>-Cats with a single string index of id.

Schema and deploy

serverless-appsync-plugin, by default, expects a schema.graphql file at the root of your project. For brevity I'm just going to add a single type and a query.

# schema.graphql

type Cat {
  id: ID!
  name: String!
}

type Query {
  cat(id: ID!): Cat
}

schema {
  query: Query
}

Now, we're set to deploy our service.

npx serverless deploy --stage dev

It's that simple!

Serverless configuration

service:
  name: rapid-prototyping

plugins:
  - serverless-appsync-plugin

provider:
  name: aws
  runtime: nodejs8.10
  stage: ${opt:stage, 'dev'}
  region: us-east-1

custom:
  accountId: # Your AWS account id
  appSync:
    name: ${self:provider.stage}-rapid-prototypes-ftw
    authenticationType: API_KEY
    mappingTemplates:
      - dataSource: ${self:provider.stage}Cats
        type: Query
        field: cat # maps to the query name in your schema
        request: Cat.request
        response: Cat.response
    serviceRole: 'Dynamo-AppSyncServiceRole'
    dataSources:
      - type: AMAZON_DYNAMODB
        name: ${self:provider.stage}Cats
        description: 'Cats table'
        config:
          tableName: ${self:provider.stage}-Cats
          serviceRoleArn: 'arn:aws:iam::${self:custom.accountId}:role/Dynamo-AppSyncServiceRole'

resources:
  Resources:
    CatsTable:
      Type: 'AWS::DynamoDB::Table'
      Properties:
        TableName: ${self:provider.stage}-Cats
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

Related posts