AWS AppSync code-centric development using CloudFormation


AWS AppSync is a serverless GraphQL backend-as-a-service. Most of the articles online show its usage by going on AWS console and configuring via Web UI which is not code-centric approach. I would like to show more code-driven approach by employing a technique called Infrastructure as Code using AWS CloudFormation. CloudFormation allows you to specify all AWS resources in the form of code and automate your deployments via command line. In this way all infrastructure resources needed by your application become part of git repository - version controlled. You can reproduce whole stack (set of related resources) in the cloud with a single command instead of repeating steps manually via GUI. Once the initial stack is created, you can easily deploy changes in your infrastructure by modifying the template file and then executing commands via the command line. This approach gives you full direct control via code and it’s very flexible to integrate with the rest of the AWS ecosystem.

AWS AppSync code-centric development using CloudFormation
AWS AppSync code-centric development using CloudFormation

I will show step by step how to create and deploy production grade AppSync GraphQL API using DynamoDB all from the comfort of your editor and terminal.

1. Define GraphQL schema

Create a sample schema having one query to get contacts and one mutation to add a new contact and save the file as schema.graphql in the main project directory.

type Contact {
  username: String!
  fullName: String!
  email: String!
  phone: String
}

input ContactInput {
  fullName: String!
  email: String!
  phone: String
}

type Query {
  getContacts: [Contact!]!
}

type Mutation {
  addContact(username: String! contact: ContactInput!): Contact
}

type Schema {
  query: Query
  mutation: Mutation
}

 

2. Add Resolver Mapping templates

Resolver mapping template tells AppSync how to translate an incoming GraphQL request into instructions for your backend data source, and how to translate the response from that data source back into a GraphQL response. Create separate request and response mapping templates into different files for each resolver.

## request mapping template for addContact mutation which maps to PutItem request for DynamoDB.
{
  "version": "2017-02-28",
  "operation": "PutItem",
  "key": {
    "username": $util.dynamodb.toDynamoDBJson($ctx.args.username)
  },
  "attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.contact)
}
## response mapping template for addContact mutation which returns newly added contact object.
$util.toJson($ctx.result)
## request mapping template for getContacts query which maps to Scan request for DynamoDB.
{
  "version": "2017-02-28",
  "operation": "Scan"
}
## response mapping template for getContacts query which returns all contacts.
$util.toJson($ctx.result.items)

 

3. Specify resources in CloudFormation template

Here is CloudFormation template in YAML format which is more readable than its JSON counterpart. It specifies details about AppSync API, API Key, GraphQL Schema, DynamoDB Table, Data Sources, GraphQL Resolvers & giving access to DynamoDB Table using IAM Role.

AWSTemplateFormatVersion: '2010-09-09'
Description: Create AppSync GraphQL API using DynamoDB

Parameters:

  ApiName:
    Type: String
    Description: Name of the API - used to generate unique names for resources
    MinLength: 3
    MaxLength: 25
    AllowedPattern: '^[a-zA-Z][a-zA-Z0-9\-]*$'

Resources:

  GraphQLApi:
    Type: AWS::AppSync::GraphQLApi
    Properties:
      Name: !Sub ${ApiName}-graphql-api
      AuthenticationType: API_KEY

  ApiKey:
    Type: AWS::AppSync::ApiKey
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId

  GraphQLSchema:
    Type: AWS::AppSync::GraphQLSchema
    Properties:
      ApiId: !GetAtt GraphQLApi.ApiId
      # path to graphql schema file
      DefinitionS3Location: schema.graphql

  ContactsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub ${ApiName}-contacts
      KeySchema:
      - AttributeName: username
        KeyType: HASH
      AttributeDefinitions:
      - AttributeName: username
        AttributeType: S
      BillingMode: PAY_PER_REQUEST

  ApiRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - appsync.amazonaws.com
          Action:
          - sts:AssumeRole
      Policies:
      - PolicyName: !Sub ${ApiName}-dynamo-access
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:DeleteItem
            - dynamodb:UpdateItem
            - dynamodb:Query
            - dynamodb:Scan
            - dynamodb:BatchGetItem
            - dynamodb:BatchWriteItem
            Resource: !GetAtt ContactsTable.Arn

  ContactsTableDataSource:
    Type: AWS::AppSync::DataSource
    Properties:
      Name: ContactsTableDataSource
      Type: AMAZON_DYNAMODB
      ServiceRoleArn: !GetAtt ApiRole.Arn
      DynamoDBConfig:
        AwsRegion: !Ref AWS::Region
        TableName: !Ref ContactsTable
      ApiId: !GetAtt GraphQLApi.ApiId

  QueryGetContactsResolver:
    Type: AWS::AppSync::Resolver
    Properties:
      TypeName: Query
      FieldName: getContacts
      DataSourceName: !GetAtt ContactsTableDataSource.Name
      # path to request mapping template file for getContacts
      RequestMappingTemplateS3Location: mapping-templates/getContacts.request.vm
      # path to response mapping template file for getContacts
      ResponseMappingTemplateS3Location: mapping-templates/getContacts.response.vm
      ApiId: !GetAtt GraphQLApi.ApiId

  MutationAddContactResolver:
    Type: AWS::AppSync::Resolver
    Properties:
      TypeName: Mutation
      FieldName: addContact
      DataSourceName: !GetAtt ContactsTableDataSource.Name
      # path to request mapping template file for addContact
      RequestMappingTemplateS3Location: mapping-templates/addContact.request.vm
      # path to response mapping template file for addContact
      ResponseMappingTemplateS3Location: mapping-templates/addContact.response.vm
      ApiId: !GetAtt GraphQLApi.ApiId
  
Outputs:

  GraphQLApiEndpoint:
    Description: GraphQL API URL
    Value: !GetAtt GraphQLApi.GraphQLUrl

  ApiKey:
    Description: API Key
    Value: !GetAtt ApiKey.ApiKey

 

4. Deploy using AWS CLI

First, install AWS CLI and configure it by specifying account’s security credentials and region information. Provide the profile option in all commands if you want to use a particular profile configuration otherwise, it will use the default profile.

Create an S3 bucket for uploading code files.

aws s3 mb s3://contacts-dev-codes  --profile faisal

Execute package command to upload local artifacts referred in the given template to the specified S3 bucket. This will generate a new template which refers to S3 files instead of local files.

aws cloudformation package --s3-bucket contacts-dev-codes --template-file template.yml --output-template-file packaged-template.yml --profile faisal

Execute deploy command with the newly generated template to create infrastructure resources. Also, provide stack name and parameters required in the template.

aws cloudformation deploy --stack-name contacts-dev --template-file packaged-template.yml --capabilities CAPABILITY_IAM --parameter-overrides ApiName=contacts-dev --profile faisal

Now you have completely functional AppSync GraphQL API with DynamoDB in AWS Cloud. You can use package and deploy commands repeatedly whenever you make changes in the template, it will automatically create change set and execute it. If you are using Node.js you can add these commands as NPM scripts to package.json of a project for convenience.

I have created GitHub repository which contains all boilerplate code with complete setup required to kickstart AppSync project.


date_range Mon Jan 14 2019
share Share this article on  twitter   facebook

Comments

Loading