Production Ready GraphQL for AWS & Serverless
Roman Abdulmanov
Posted on September 23, 2022
What is this post about?
Many articles have been written on implementing a serverless GraphQL API using AWS. But there is almost nothing about how to make it production ready for websites.
This article will also be valid for API Gateway.
Tools
You can use different tools: AWS SAM, Serverless Framework, Pulumi, Terraform, pure AWS CDK, or something else. Ultimately, it's not all that important. The main thing is that you do it not manually. Everything must be automated for repeatability. Otherwise, you won't be able to build a reliable CI/CD process.
I like this plugin that will make your life easier. It will help you set up AppSync, manage API keys, configure WAF, etc.
Typical architecture
Most articles end up with a below architecture, service or website that directly calls the AppSync API:
The downside of this solution is that the API is susceptible to DDoS attacks, which can significantly increase your monthly bills.
Improved architecture
The obvious solution is to add a firewall with request limiting:
It will also help if you want to block GraphQL introspection:
And this could be the end, but this solution has one unpleasant flaw for websites: preflight request.
Preflight request
Since your site and API are on different domains, the browser will make 1 additional request to the server BEFORE any other requests.
This may come as an unexpected surprise, since the initial request can take almost twice as long.
Example:
Details (additional 456 milliseconds to wait for the request to complete):
As I already wrote, since your domain is hosted on mydomain.com
, and the API is on https://[ID].appsync-api.[REGION].amazonaws.com
, the browser will attempt to do additional validation for CORS by sending this request before the first call to the domain hosting the API.
The browser caches this request for a while, but if this is a public page and many people visit this site for the first time, then they will experience this problem.
It also starts repeating after some time when the cache expires.
Potential solution №1
The first thing that comes to mind if you are unfamiliar with preflight requests is that they can probably be disabled if the backend sets some header.
Unfortunately this is not possible.
Potential solution №2
Maybe, I can use a simple request to workaround this restriction?
This will not work if you have non-standard headers (e.g. Authorization), you will also have to set the wrong content-type. Even if it works, this does not look like a production ready solution and can be blocked by AWS / browsers anytime.
Potential solution №3
You can try to use iframe technique but it will not work for non-subdomain urls (we will talk about this later), and it will make the loading even slower because instead of a very fast request, you will have slow iframe loading.
Potential solution №4
Can I use custom domains?
Unfortunately, this is not possible too. You can't use the same domain for API (e.g. just mydomain.com
). It will work only with sub-domain (e.g. api.mydomain.com
). Browsers check for the same URL origin, so you can't use anything other than mydomain.com/[something]
.
Solution
If we are talking about AWS and Serverless, the most affordable solution is to use AWS CloudFront.
We can create two behaviors and redirect traffic to the correct Origin depending on the path pattern:
For this to work, CloudFront must have the correct certificates configured to accept requests using your DNS name.
This solution will solve the original problem, there are no more preflight requests or some kind of hacks.
The edge location of the servers would be an additional bonus. Which will reduce the connection time of clients with the endpoint.
The additional configuration of CloudFront Origin Shield will allow you to speed up traffic to AppSync or other service by using an internal faster network.
Caveat №1
We have implemented this architecture:
Since WAF works based on the IP address of the caller, it will not be the client's address but CloudFront's. Therefore, the request limit can be hit much faster and affect all users in the same location.
To solve this, we can use the X-Forwarded-For
header to receive client IPs from CloudFront:
But this will make our AppSync API insecure, and if someone finds out the unique address of our endpoint, they can attack through it.
Therefore, it is more reliable to make two firewalls. Migrate the old one to handle requests in front of CloudFront. And make a new one between CloudFront and AppSync that will prohibit all requests not from CloudFront (make it directly inaccessible):
To set up New WAF, you can deny all requests by default and allow only those that contain some secret header with a value that is configured on CloudFront. WAF documentation for details.
CloudFormation:
Rules:
- name: Secret
action: Allow
priority: 1
statement:
byteMatchStatement:
searchString: [secret]
fieldToMatch:
singleHeader:
name: Secret
textTransformations:
- priority: 0
type: NONE
positionalConstraint: 'EXACTLY'
Caveat №2
You should use the default сache policy (see below), even if you don't cache anything (POST requests are never cached). Because if you disable it, request compression will not work.
Caveat №3
If you have more than one endpoint, you will not be able to set up a redirect because you cannot have two identical path patterns: '/graphql'.
To resolve this, you can use some unique paths and setup CloudFront functions to redirect requests to the correct path for the Origin.
CloudFormation:
CloudFrontGraphQLFunction:
Type: AWS::CloudFront::Function
Properties:
Name: ${self:provider.stackName}-graphql-redirect
AutoPublish: true
FunctionCode: !Sub |
function handler(event) {
var request = event.request;
request.uri = "/graphql"
return request;
}
FunctionConfig:
Runtime: cloudfront-js-1.0
Caveat №4
When developing locally, you may have problems with the frontend and CORS.
This is because the first response of the preflight request from localhost to the API does not contain all the required headers, even if you set the appropriate policy.
CloudFront will add access-control-allow-origin
, but Chrome also needs (-headers and -methods).
To avoid the need to use additional extensions like this. You can configure additional headers using the CloudFront function:
CloudFrontGraphQLCorsFunction:
Type: AWS::CloudFront::Function
Properties:
Name: ${self:provider.stackName}-graphql-cors
AutoPublish: true
FunctionCode: !Sub |
function handler(event) {
var response = event.response;
var headers = response.headers;
headers['access-control-allow-origin'] = {value: "*"};
headers['access-control-allow-headers'] = {value: "*"};
headers['access-control-allow-methods'] = {value: "*"};
return response;
}
FunctionConfig:
Runtime: cloudfront-js-1.0
Conclusion
This approach should work not only with AppSync but also with other services, such as API Gateway.
If you have any questions, please write, and I will gladly try to answer.
Thanks
Posted on September 23, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.