Production Ready GraphQL for AWS & Serverless
Tags: aws, serverless, appsync, graphql
Published: 2022-09-23 14:06:58

## 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] (https://www.serverless.com/plugins/serverless-appsync-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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c03xhke6ty90ppq9v7q7.png)

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:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fvcv08dui6i8vvfbrfmh.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vrdecp069u50gv732aeu.png)


It will also help if you want to block GraphQL introspection:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yudcgf9bki410a2gs7ih.png)

And this could be the end, but this solution has one unpleasant flaw for websites: [preflight request](https://developer.mozilla.org/en-US/docs/Glossary/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:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ob6ro9902nqg1pd3ikdc.png)

Details (additional 456 milliseconds to wait for the request to complete): 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wyttx7hhgm8h3c9k5zcd.png)

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](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests) 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](https://stackoverflow.com/a/31555285/1235413) 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](https://aws.amazon.com/ru/blogs/mobile/introducing-custom-domain-names-for-aws-appsync-apis/)? 


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:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/68xku9udz930m1434ekw.png)

_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](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/origin-shield.html) 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:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hq6uq45ex436rp39yxwp.png)

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:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/brk2b1ds9v9hyl62k1ap.png)

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):


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p6haxg53t4ymqv76y9hx.png)

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](https://docs.amazonaws.cn/en_us/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-webacl.html) for details.

CloudFormation:

```yml
  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. 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gj3rcqryor652ech64et.png)

## 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](https://aws.amazon.com/en/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/) to redirect requests to the correct path for the Origin.

CloudFormation:

```yml
  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.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/am3jao7xeclkd7g5fl6a.png)

CloudFront will add `access-control-allow-origin`, but Chrome also needs (-headers and -methods). 

To avoid the need to use additional extensions like [this](https://chrome.google.com/webstore/detail/allow-cors-access-control/lhobafahddgcelffkeicbaginigeejlf). You can configure additional headers using the CloudFront function:

```yml
  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