AWS AppSync GraphQL API with Golang Lambda source

GraphQL has been a buzzword for a while now. I immediately fell in love with it when GitHub announced a public GraphQL API. A few weeks ago AWS introduced AppSync, a serverless GraphQL with support for custom data sources using AWS Lambda. Together with the recently introduced Go support for AWS Lambda, this is just awesome!

Update (New): If you enjoy GraphQL and AppSync or just want to see the latest guide, you should see the updated document and example project about appsync-resolvers, CloudFormation and SAM. The setup can be cleaned up a even more and uses a generic approach for resolver handling.

Update (Old): This guide uses the AWS CLI to setup all AppSync related resources. Amazon recently introduced CloudFormation support for AppSync! Make sure to check the updated guide to have an even more simple setup!

Introduction

Parsing an RSS feed is never fun; XML is never fun as well. To enable easy access to my RSS feed, I wanted to create a simple GraphQL API using AWS AppSync. Based on the existing example project to deploy an AWS Lambda function using Go, I create a project to wrap up everything with AppSync: appsync-graphql-rss-proxy-go

The serverless AWS configuration with Serverless Application Model is dead simple, but the lack of CloudFormation support for AWS AppSync makes the deployment of the API a bit tricky:

Dependencies

To create the AppSync GraphQL API, you will need the awscli. On a macOS system, just install it with homebrew:

$ > brew install awscli

Install Go packages

For building the Golang Lambda function, two Go dependencies are needed. Use go get to install them.

# make dependencies

$ > go get github.com/SlyMarbo/rss
$ > go get github.com/axgle/mahonia

Configure Environment

Create a .env file inside the project folder with your basic configuration. As AppSync is not available in all regions, check availability first. You can use us-west-1 as default.

AWS_ACCOUNT_ID=1234567890
AWS_BUCKET_NAME=your-bucket-name-for-cloudformation-package-data
AWS_STACK_NAME=your-cloudformation-stack-name
AWS_REGION=us-west-1

Deploy

The included main.go file fetches an RSS feed, parses the data, and returns a simple list of the most recent items:

type Post struct {
	URL   string    `json:"url"`
	Title string    `json:"title"`
	Date  time.Time `json:"date"`
}

func handleRequest(req events.APIGatewayProxyRequest) (interface{}, error) {
	feed, err := rss.Fetch(os.Getenv("URL"))

	if err != nil {
		return nil, errors.New("Cannot fetch RSS feed")
	}

	list := []Post{}

	for _, item := range feed.Items {
		list = append(list, Post{item.Link, item.Title, item.Date})
	}

	return list, nil
}

Per default, the Lambda function will crawl the RSS feed of this domain, but you can configure the URL of the RSS feed in the environment variables inside the template.yml CloudFormation configuration file.

Thanks to the Serverless Application Model, deploying your AWS Lambda function is dead simple these days:

# Create S3 Bucket
$ > make configure

# Upload data to S3 Bucket
$ > make package

# Deploy CloudFormation Stack for Lambda function
$ > make deploy

# Get Lambda information
$ > make outputs

[
  {
    "OutputKey": "HandlerArn",
    "OutputValue": "arn:aws:lambda:us-east-1:1234567890:function:your-cloudformation-stack-name-Handler-S5V98FXUK8QT",
    "Description": "ARN for Go Handler"
  },
  {
    "OutputKey": "RoleArn",
    "OutputValue": "arn:aws:iam::1234567890:role/appsync-graphql-rss-proxy-go-Role-18N2ACCHT0MRM",
    "Description": "ARN for IAM Role"
  }
]

Store the OutputValue for HandlerArn and RoleArn somewhere, you will need them in a few steps.

AppSync

Currently, neither CloudFormation is supported by AppSync, nor is an integration with AWS Serverless Application Model in place. But luckily, the AWS CLI is here to safe the day! Run make create-api to setup the basics for your AppSync GraphQL API:

$ > make create-api

{
  "graphqlApi": {
    "name": "appsync-graphql-rss-proxy-go",
    "apiId": "5qemu9oyli88sho8tl5rh0ejpil",
    "authenticationType": "API_KEY",
    "arn": "arn:aws:appsync:us-east-1:1234567890:apis/5qemu9oyli88sho8tl5rh0ejpil",
    "uris": {
      "GRAPHQL": "https://5qemu9oyli88sho8tl5rh0ejpil.appsync-api.us-east-1.amazonaws.com/graphql"
    }
  }
}

Store the URL for GRAPHQL somewhere safe; you will need it for sending requests to your GraphQL API afterward. For all upcoming commands you will need the apiId value, so copy it to your clipboard or write it down somewhere as well.

Upload GraphQL Schema

The example project on GitHub includes a GraphQL schema; it’s stored in schema.graphql and has basic types to query items in an RSS feed.

type Post {
  url: String
  title: String
  date: String
}

type Query {
  feed: [Post]
}

schema {
  query: Query
}

You need to use the graphqlApi.apiId parameter from the previous command to upload the schema.graphql file to AWS and update your AppSync API:

$ > API_ID=5qemu9oyli88sho8tl5rh0ejpil \
    make create-api-schema

{
  "status": "PROCESSING"
}

After having the API in place and the GraphQL schema configured, it’s time to setup the custom data sources for your API!

When you deployed your Lambda function and ran make outputs, you saw a few information about the CloudFormation stack, right? Now it’s time to make use of them! Replace the ARN for the Lambda function and the ARN for the IAM Role in the next command:

$ > API_ID=5qemu9oyli88sho8tl5rh0ejpil \
    LAMBDA=arn:aws:lambda:us-east-1:1234567890:function:your-cloudformation-stack-name-Handler-S5V98FXUK8QT \
    ROLE=arn:aws:iam::1234567890:role/appsync-graphql-rss-proxy-go-Role-18N2ACCHT0MRM \
    make create-api-data-source

{
  "dataSource": {
    "dataSourceArn": "arn:aws:appsync:us-east-1:1234567890:apis/5qemu9oyli88sho8tl5rh0ejpil/datasources/RSSProxy",
    "name": "RSSProxy",
    "type": "AWS_LAMBDA",
    "serviceRoleArn": "arn:aws:iam::1234567890:role/appsync-graphql-rss-proxy-go-Role-18N2ACCHT0MRM",
    "lambdaConfig": {
      "lambdaFunctionArn": "arn:aws:lambda:us-east-1:1234567890:function:appsync-graphql-rss-proxy-go-Handler-S5V98FXUK8QT"
    }
  }
}

With the custom data source in place, you can now connect it to your GraphQL schema as a custom resolver:

$ > API_ID=jrwgynjw6jfmdevmqlrknewqna \
    make create-api-resolver

{
  "resolver": {
    "typeName": "Query",
    "fieldName": "feed",
    "dataSourceName": "RSSProxy",
    "resolverArn": "arn:aws:appsync:us-east-1:1234567890:apis/5qemu9oyli88sho8tl5rh0ejpil/types/Query/resolvers/feed",
    "requestMappingTemplate": "{ \"version\" : \"2017-02-28\", \"operation\": \"Invoke\", \"payload\": $util.toJson($context.arguments) }",
    "responseMappingTemplate": "$util.toJson($context.result)"
  }
}

Using the GraphQL API

To test your GraphQL API you will need an API key. Luckily this can be created using the CLI as well.

$ > API_ID=jrwgynjw6jfmdevmqlrknewqna \
    make create-api-key

{
  "apiKey": {
    "id": "da2-nuyzhanm5bcptga6yilkj7zluy",
    "expires": 1519725600
  }
}

You can use curl to test your API or use a GraphQL UI. Just make sure to configure the needed authentication header with your API Key:

$ > curl \
    -XPOST https://5qemu9oyli88sho8tl5rh0ejpil.appsync-api.us-east-1.amazonaws.com/graphql \
    -H "Content-Type:application/graphql" \
    -H "x-api-key:da2-nuyzhanm5bcptga6yilkj7zluy" \
    -d '{ "query": "query { feed { title url } }" }' | jq

{
  "data": {
    "feed": [
      {
        "title": "Deploy Golang Lambda with AWS Serverless Application Model",
        "url": "https://sbstjn.com/golang-lambda-with-aws-sam-serverless-application-model.html"
      },
      {
        "title": "68% Mechanical Keyboard with 68Keys.io",
        "url": "https://sbstjn.com/build-your-own-mechanical-keyboard.html"
      }
    
      [...]
  }
}

That’s awesome, right? What’s your setup to deploy and maintain a GraphQL API using AWS AppSync? Let me know on Twitter what you this about this approach and if you know of any way to already do this with CloudFormation!


View on GitHub Source code is published using the MIT license.