I used AWS AppSync to create a GraphQL endpoint. I am interested in migrating towards GraphQL as a service layer within my current company. Plus I am always keen to add additional strings to my AWS bow, so I decided to make a small proof of concept. Unfortunately, the AWS documentation is somewhat of a rabbit hole. I was quite surprised how confusing this process was. I expected it to be more accessible. I am sharing my process here because I am sure other engineers will have similar issues.

Resources before you start

If your new to some of these topics the following links may be useful before you start:

Building the proof of concept - A Basic Overview

I have a REST API endpoint which currently needs a token to access the data. I wanted to wrap this endpoint to allow unauthenticated public access as an experiment so I made a small POC.

Create a small React app using create-react-app

For this tutorial, we will consume our newly created GraphQL endpoint with a simple React application. We will also need a location for the generated assets which come from the AWS Amplify CLI setup process.

npx create-react-app my-app
cd my-app
npm start

Configure Amplify CLI and initialize a new Amplify project

The Amplify Command Line Interface (CLI) is a unified toolchain to create AWS cloud services. First, we need to install the AWS Amplify CLI globally. For more information on this step, you can review the official AWS guide, Install the Amplify CLI.

npm install -g @aws-amplify/cli

Next, we need to configure the AWS Amplify CLI so it can provision AWS services for us.

You may need an admin to create an IAM user for this process and that IAM user will need "AdministratorAccess" to create resources for you like AppSync, Cognito etc.

amplify configure

Use these settings during the configuration:

  • region: YOUR_SERVER_LOCATION
  • user name: YOUR_USER_NAME
  • accessKeyId: YOUR_ACCESS_KEY_ID
  • secretAccessKey: YOUR_SECRET_ACCESS_KEY
  • Profile Name: default

Now you have configured your Amplify CLI we can start to provision AWS services from the command line. Let's initialize a new Amplify project first.

amplify init

I used these settings during the initialization, but this may differ for your needs:

  • Enter a name for the project: YOUR_PROJECT_NAME
  • Enter a name for the environment: dev
  • Choose your default editor: Visual Studio Code
  • Choose the type of app that you're building: javascript
  • What javascript framework are you using: react
  • Source Directory Path: src
  • Distribution Directory Path: build
  • Build Command: npm run-script build
  • Start Command: npm run-script start
  • Do you want to use an AWS profile: Yes
  • Please choose the profile you want to use: default

After working through the wizard, the CLI will start to provision AWS services for your application including S3 buckets, several IAM roles and a CloudFormation stack.

The struggle of creating an unauthenticated GraphQL API

This is the part that took the most significant amount of trial and error, creating the actual API. I researched for several hours trying to understand this process. In the end, I found a few resources outside of the AWS documentation, which helped me get this step working correctly.

The base issue is that all requests to the AppSync API need to be authenticated even if you do not want the resource to be protected. I did not find a way to disable the need for authentication; instead, it seems I had to use a mix of IAM Roles and Cognito User Pools.

With this setup, the user is issued a JWT token even if they are not logged in, and this JWT token will allow them to access the Graph API.

The AWS documentation explains that it is possible to have this kind of set up but seem to lack details on how. Perhaps a more experienced "AWS Certified Solutions Architect" would have made more sense of this process, but the countless searches via Google and conversations on GitHub lead me to believe that many people have struggled with this seemingly simple task.

Unauthenticated identities - Amazon Cognito can support unauthenticated identities by providing a unique identifier and AWS credentials for users who do not authenticate with an identity provider. If your application allows customers to use the application without logging in, you can enable access for unauthenticated identities.

The following documents allowed me to piece together the answers I needed, I put them here for reference for anyone who finds difficulty in this process.

Scaffolding the GraphQL API with the AWS Amplify CLI

Eventually, after much trial and error I did the following steps. First, we bootstrap the application using AWS Amplify CLI.

amplify add api

I used these settings to add the GraphQL API selecting the "Amazon Cognito User Pool" option.

  • Please select from one of the below mentioned services: GraphQL
  • Provide API name: YOUR_API_NAME
  • Choose the default authorization type for the API: Amazon Cognito User Pool
  • Do you want to configure advanced settings for the GraphQL API: No, I am done.
  • Do you have an annotated GraphQL schema: No
  • Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description)
  • Do you want to edit the schema now? Yes

Then I quickly added my schema and continued. The CLI will provision all the necessary AWS services using the information above.

Quick Tip: Converting a JSON REST response to a GraphQL Schema

I used an online tool (JSON to GraphQL Schema) to convert my JSON response to a GraphQL schema, it needed some small tweaks, but the bulk of the scaffolding was handled.

Here is an example schema where you can see I pass an id and type to the getChannelById method.

type Channel {
  id: ID!
  type: String!
  requestId: String
  entity: Entity
  ack: String
}

type Entity {
  type: String
  id: String
  name: String
  storeCode: String
  address1: String
  city: String
  zip: String
  country: String
  open247: String
  hashCode: String
  defaultChannel: String
  deliveryMethods: [String]
  operatingHours: [OperatingHours]
  geometry: Geometry
}

type Geometry {
  latitude: String
  longitude: String
  radius: String
}

type Interval {
  open: String
  closed: String
}

type OperatingHours {
  dayOfWeek: String
  interval: Interval
}

type Query {
  getChannelById(id: ID!, type: String!): Channel
}

After Amplify finishes provisioning the services needed for the above setup, we need to push the application and make some additional manual changes.

amplify push

Switch the application to use IAM roles combined with an identity pool

The next part of this tutorial was mostly published by Nader Dabit on his GitHub page. Nader works at AWS as a Developer Advocate at AWS Mobile, so I feel comfortable with his workaround to achieve our goal.

We bootstrapped our application using Amazon Cognito User Pool's, and now we need to switch to using IAM roles and configure those roles manually to set the permission on each endpoint.

Switch the AppSync API settings to use IAM roles

In the AWS Management Console go to the AWS AppSync service:

  • Click on YOUR_API_NAME.
  • Click on Settings.
  • Change the Default authorization mode to AWS Identity and Access Management (IAM).

Now we need to make changes in the React app, open the aws.exports.js file and modify aws_appsync_authenticationType and change it to AWS_IAM.

Modify the Amazon Cognito User Pool

In the AWS Management Console go to the Amazon Cognito service:

  • Click on Manage Identity Pools.
  • Click on the newly created identity pool.
  • Click on Edit identity pool.
  • Under Unauthenticated identities check Enable access to unauthenticated identities.
  • Click Save Changes.

Make a note of the two newly created IAM roles "Unauthenticated role" and "Authenticated role".

Create a policy for the unauthenticated IAM role

In the AWS Management Console go to the Identity and Access Management (IAM) service:

  • Click on Policies.
  • Click on Create policy.
  • Click on JSON.
  • Paste and modify the policy below.
  • Click on Review policy.
  • Give the policy a Name and a Description and click Create policy.
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "appsync:GraphQL",
            "Resource": [
              "arn:aws:appsync:<REGION>:<ACCOUNTID>:apis/<APIID>/types/Query/*"
              "arn:aws:appsync:<REGION>:<ACCOUNTID>:apis/<APIID>/types/Query/getChannelById"
            ]
        }
    ]
}

The above policy shows two examples of allowing access to Query types. It makes sense to have a policy where you can maintain a list of endpoints which you want to allow public access. You can also specify individual fields, to get a deeper understanding, you can switch between the "Visual Editor" and "JSON" view to understand the possibilities. To learn more go to AWS AppSync - AWS_IAM Authorization and IAM Policy - Advanced Workflows for more examples.

Attach the new policy to the IAM Role

Now we have created our policy we can attach it to the unauthorized IAM role. You can also create another policy for authorized users and attach it to the authorized IAM role.

In the AWS Management Console go to the Identity and Access Management (IAM) service:

  • Click on Roles.
  • Locate and click on the newly created unauthorized IAM role which you noted down earlier.
  • Click on Attach policies.
  • Locate the newly created policy.
  • Click on the checkbox and click Attach Policy.

The article on Medium that I mentioned earlier had an excellent summary for these policies which I have included below:

For AuthRole — Add all the CRUD APIs — list, create, update and delete (if you intend yxour authenticated user to be able to do all these of-course). Note — List and Get APIs are under “Query” and the Create, Update and Delete are under “Mutation”.

For UnAuthRole — Add ONLY the — list APIs as you want your Unauthenticated users to have only read-only access to the APIs/data

<YOUR_ACCOUNT_ID> is found here : https://console.aws.amazon.com/billing/home

<YOUR_API_ID> is found under the settings tab of the GraphQL API that you are working with.

Fingers crossed, you made it this far

Now when you log in to AWS AppSync, you should see the newly created API schema, and you should be able to run queries against your GraphQL endpoint.

Running queries will yield no results because there is no data behind the GraphQL endpoint.

Let's attach the getChannelById Query to our existing REST API.

Create a new Data Source

As we want to talk to an existing REST endpoint, we need to add the existing Data Source.

In the AWS Management Console go to the AWS AppSync service:

  • Click on YOUR_API_NAME.
  • Click on Data Sources.
  • Click on Create data source.
  • Enter a Data source name.
  • Enter HTTP endpoint under Data source type.
  • Enter the root domain for your existing API (eg: api.example.com) under HTTP endpoint.
  • Click Create.

Attach a Resolver to the existing REST API data source

Now we have a data source we need to create two templates for handling the request and the response from the existing API so we can pass data through our GraphQL API to the existing REST API.

In the AWS Management Console go to the AWS AppSync service:

  • Click on YOUR_API_NAME.
  • Click on Schema.
  • In the Resolvers panel, locate the Query you want to resolve.
  • Click Attach.
  • On the next screen select the Data source we just created and modify the mapping templates. I included my templates below for reference as I had a fairly interesting request template.

Request mapping template

## Specify the resourcePath endpoint and pass variables into the request.
## I pass an Authorization key for my existing REST application.
## I pass two bits of data with the request, one in the URL and one in the Query params
## My endpoint expects this format: /api/v1/channels/1234?type=online_store

{
  "version": "2018-05-29",
  "method": "GET",
  "resourcePath": "/api/v1/channels/$ctx.args.id",
  "params":{
      "query": {
        "type": $util.toJson($ctx.args.type)
      },
      "headers": {
          "accept": "application/json",
          "X-Auth-Key": "MY_APPLICATION_AUTHORIZATION_KEY"
      }
  }
}

Response mapping template

## Raise a GraphQL field error in case of a data source invocation error
if($ctx.error)
  $util.error($ctx.error.message, $ctx.error.type)
end

## If the response is not 200 then return an error. Else return the body **
if($ctx.result.statusCode == 200)
    $ctx.result.body
else
    $utils.appendError($ctx.result.body, "$ctx.result.statusCode")
end

Now you should be able to make requests while passing variables to your GraphQL endpoint.

AppSync Resolvers use a Request/Response template pattern similar to API Gateway. Like API Gateway, the template language is Apache Velocity. In an AppSync resolver, you can examine the incoming authentication information (such as the IAM username) in the context variable. You can then compare that username against an owner field in the data being retrieved.

Additional reading: Resolver Mapping Template Reference

Create the GraphQL client Query

Below is the syntax needed to pass data into our GraphQL Query, we can now pass id and type to the Query and also we can specify the nested fields we want to be returned.

# Client GraphQL Query
query MyQuery($id: ID!, $type: String!) {
  getChannelById(id: $id, type: $type) {
    entity {
      name
      operatingHours {
        interval {
          closed
          open
        }
        dayOfWeek
      }
      zip
      type
      storeCode
      open247
      id
      hashCode
      geometry {
        latitude
        longitude
        radius
      }
      deliveryMethods
      defaultChannel
      country
      city
      address1
    }
    type
    id
    requestId
    ack
  }
}

And we can pass a small data object to the above query to test it in the GraphQL query explorer.

# Client data passed to the query
{
    "id": 52,
    "type": "online_store"
}

Using the GraphQL endpoint to get data in our React app

Now we can finally get the data into our bare-bones React application after we install the aws-amplify package.

npm install aws-amplify

For a quick test open src/app.js and place the following code there. You will have to modify this for your needs based on your API, but hopefully, this example is detailed enough to understand the complete process.

import Amplify, { API } from 'aws-amplify';
import aws_exports from './aws-exports';
import { getChannelById } from './graphql/queries';

Amplify.configure(aws_exports);

(async () => {
  const result = await API.graphql({
    query: getChannelById,
    variables: { id: 58, type: 'online_store' },
    authMode: aws_exports.aws_appsync_authenticationType,
  });
  console.log('Result:', result.data);
})();

That's a Wrap!

Well, that concludes another rather intensive AWS experience, I hope I have included enough detail here to guide you through the set up process. My personal experience with this proof of concept took me two whole days of researching and trying different solutions which is the reason why I put this post together. If you manage to achieve the same outcome in less than two days, then pat yourself on the back! If you have any comments or feedback, please leave them below.