AWS AppSync with Serverless Lambda only solution

Barend Bootha

In this article I’m going to cover how we use AppSync with Lambda’s and our slightly unusual way of using the service and the reasons behind our implementation.

What is AppSync

AppSync is quite an understated service, I’ve never really paid attention to it mostly as it wasn’t described really well. Something along the lines of

“GraphQL as a service”

GaaS?

It is that, but provides a layer of security, supports real time subscriptions and have offline support. There is quite a bit you get for free without the need of spinning up a server, and no you can’t really cover all of that with plain Serverless, as it stands today you can’t manage Subscriptions when you run GraphQL in Serverless without AppSync.

This article is not to try and sell you on AppSync, it’s more about our experiences with it thus far.

AppSync quick intro and VTL

If you’re familiar with AppSync and VTL you can skip forward.

AppSync summed up

In its simplest form, you’d define your own GraphQL schema and bind resolvers to your graph nodes, these nodes can then resolve to any of the following AWS Services

  • DynamoDB
  • RDS
  • ElasticSearch
  • AWS Lambda
  • HTTP

These resolvers are written in Apache Velocity Templates (VTL), something basic might look like this

An example of a DynamoDB getItem operation

Each resolver consists of a request and a response template. To be honest I’m not a fan of dumping logic into VTL, I know why AWS did it… Speed, plain and simple. But if you have some sort of business logic it has to sit somewhere. A simple process to check if one record exists before you update another? Try doing that in VTL, you’re not going to like it.

Why we chose AppSync

We wanted/needed to create a GraphQL API, that would work well with the current EcoSystem of React/ReactN. And really really want to use GraphQL Subscriptions.

But as it stands today, we’re a really small team, we have no Site Reliability Engineers (SRE’s) and lack the knowledge of running a docker cluster. And none of us was keen on maintaining EC2 instances.

So for our initial pilot we decided to use AppSync but use it in such a way that when we grow and have more headroom, we can swap it out for something like a Docker cluster running Apollo without rewriting the entire API. It would mean AppSync would act as our “GraphQL Proxy” prior to our business logic.

AppSync and Serverless

AppSync and Serverless work well together there the plugin available does a fantastic good job provisioning your stack. However we’re big fans of Terraform, so we tend to slim down what’s created by Serverless as much as possible.

Offline dev? There is offline support via the AppSync Offline plugin however it is limited, and there are a few bugs. But for the most part you can get away with doing 90% off your dev offline, we found that we still had to deploy to a test environment to verify that things are working as expected.

What issues did we have

  • Subscriptions
  • Auth
  • Minor problems around error outputs and black box effect

One way to mitigate the black box effect on the offline plugin is to turn on debugging for AppSync offline plugin.

Using debug mode on appsync-offline plugin

Enter the Lambda zone λ

update image to have a speech bubble “things I do in lambda”

Enter the Lambda zone is a play on “Enter the Xander zone” — image from XXX

In some conversation somewhere I remember overhearing (or was part off 😂), someone said “If it can be done in a Lambda, do it in a Lambda”.

You might be asking yourself why not setup a Serverless GraphQL endpoint? It lacks one thing we really needed Subscriptions. There is no way to do that “server-less” yet. (This is the second time I’ve told this lie, there are solutions, but none are production ready see this discussion on Github)

The idea we came up with, is to resolve everything to lambda. This does have two major drawbacks.
Latency: There is going to be an overhead to resolve the request, cold starts etc… same issues you’d face with Api Gateway.
N+1 query problem: It will become clearer as you follow the article, but the lambda doesn’t have any context, of what Query / Mutation was called, only enough context to resolve a node within the graph. Also because you’re executing this in Lamda space, you cant use tools like DataLoader as it was intended, you can’t maintain state/cache in lambda world, unless you fork it off outside of your lambda…

However there are some major major benefits resolving everything to lambda.

  • Business logic would live in code that we’re familiar with and have tooling for.
  • We can still write traditional resolvers on the lambda side and resolve queries and mutations just as you would have without AppSync.
  • The above means if when we outgrow AppSync we can swap it out for Apollo without rewriting large parts of the platform.

Lambda Serverless GraphQL Context

You have to be mindful that the designers of AppSync didn’t intend for you to turn your Lamda in a resolver within a resolver. What I mean by this is.

The lambda doesn’t have context of the graph… But it does have just enough context of the node it’s resolving

You have to ensure when you resolve your node to your lambda that you provide it with enough detail to resolve the given node.

Here is how we did it…

We wrote resolvers the normal way you would have with with most GraphQL libraries.

export const resolver = {
Query: {
getOrganisation: ({ args, injector }) =>
injector.orgService.get(args.id),
},
}

And generate our mapping templates during the build process which produces the output below.

# example from a serverless.yml using the AppSync pluginmappingTemplatesLocation:
- type: Query
field: getOrganisation
resolverPath: Query.getOrganisation # internal ref only
request: query.getorganisation-request.vtl
response: common-response.vtl
dataSource: LambdaSource

The important thing to take note here is how we mapped the AppSync resolver to our Lambda Resolver.

## query.getorganisation-request.vtl{
"version": "2017-02-28",
"operation": "Invoke",
"payload": {
"field": "Query.getOrganisation",
"context": $utils.toJson($context)
}
}

When the lambda executes, we test if our resolvers can resolve the path for the intended field. If it does, go for gold.

export async function graphqlHandler(event, context) {
const {
field,
context: { identity, arguments: args, source },
} = event;
// does our lambda side resolvers contain the intended resolverPath?
if (!_.has(resolvers, field)) {
return null;
}
// resolve the node
return await
_.get(resolvers, field)({
args,
root: source,
context: {},
info: {},
injector: serviceInjector,
});
}

So one day when we outgrow AppSync we can just keep our code move it into an Apollo instance without the need for any major rework.

So what is in the lambda’s context?

This AWS Doco explains it in more detail, but in short here is what AppSync passes to the lambda.

{    
"arguments" : { ... }, // all arguments for this field
"source" : { ... }, // the resolution of the parent field
"identity" : { ... }, // contains information about the caller
"request" : { ... } // http request information
}

As you can see, there just isn’t enough information here for you to have an idea of what your API was called with. The request doesn’t contain the body of the request, but everything else you could expect like headers etc…

N+1 query problem

If you’ve been playing with GraphQL you’re familiar with the N+1 problem.

How did we address some of it? Given the following

type ModelAccountsConnection {
items: [Account]
}

type User {
id: ID
accounts: ModelAccountsConnection
}
type Account {
id: ID
name: String
}
# sample query
query
GetUsersList {
userList {
id
accounts {
items {
id
name
}
}
}
}

The above example, we can tell from the schema that we’d need the following mapping templates in AppSync

mappingTemplatesLocation:
- type: Query
field: GetUsersList
...
- type: User
field: accounts
...

You’d end up with 1 lambda execution for getting the users
and
N calls to resolve their accounts.

But what if you omit the accounts mapping in AppSync? and when you resolve your getUsers query, you return the accounts by default?

[{
"id": "303030",
"name": "barend",
"accounts": {
"items": [
{
"id": "account-230230-12",
"name": "savings"
},
{
"id": "account-3039495-00",
"name": "credit"
}
]
}
}]

If you know how your API is going to be consumed, this can be a very safe assumption to make, in the above example it saves you a round trip.

If the consumer didn’t want accounts, they’ll be retrieved by the Lambda but just never returned by AppSync. That is not ideal…

This solution is not ideal, you have to be aware of your data characteristics and how it will be consumed. This can land you in a bad spot where you might over-fetch 1000’s if not millions of records when you only wanted the parent entity.

It might be against the design of GraphQL but in some cases it might be better to define more specific queries example getUsersAndAccounts. The assumption is then safe that the consumer wanted the accounts as well.

Another caveat of this approach, is when you leave the mapping resolver in and return the accounts data. AppSync will still try resolve the nodes a catch 22 situation indeed. A potential landmine for your fellow developer.

DAX

The other solution for the N+1 issue is DAX. Putting DAX in front of frequently requested data can resolve the overhead to your datastore.

DAX doesn’t resolve the N+1 issue, but it can mitigate against the downstream load.

In Summary

AppSync gives you a lot out of the box for free, one thing that will trip up many consumers is not knowing about N+1 problem and how AppSync has no good way to deal with it.

I personally believe AppSync has a specific market, it’s geared for the quick starts, small team, small focus, prototyping / small projects. I can see how our solution might turn to mush when we hit scale and that’s why we have an exit strategy.

VTL is a terrible place to put any business logic. If you’re keen to build any kind of solid API or even a Backend for Frontend API then you’re going to have a tough time with VTL. There is already already multi page VTL templates samples on AWS’s Doc page 🤯 that is not my idea of having fun.

Resolving everything to lambda has its benefits but also has it’s drawbacks, it really depends how you’re looking at the solution and what it’s solving for you at that specific time. For us, it’s allowing our small team to focus on feature building and worry less about running and maintaining infrastructure. We’ve got a plan for the future and are aware of obstacles in our way.

Barend Bootha

Written by

Backend engineer and DevOps practitioner, current focus is a NodeJS stack hosted in AWS

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade