Using a custom domain name for AppSync APIs

I love GraphQL and serverless architectures so it shouldn't be a surprise that I'm a huge fan of AWS AppSync. It's quickly become my default starting point for any new application. While AppSync is awesome it does have a few rough edges and one of those is the endpoint hostname.

When you create a new AppSync API you'll receive an endpoint with a URL that looks like https://XXXXXXXXXXXXXXXXXXXXXXXXXX.appsync-api.XXX-XXXXXX-X.amazonaws.com/graphql

It's not particularly attractive but that's not the issue. The first part of the hostname is randomly generated by AWS when you create a new API. Having our application depend on a randomly generated URL for the API is potential disaster for the business.

You only need to look at serverless forums to find examples of people posting in desperation after someone accidentally deleted their API Gateway or AppSync API because the AWS generated URL was used in their client application and they can't re-create an API Gateway or AppSync API with the same hostname. While setting DeletionPolicy: Retain in the CloudFormation template reduces the risk I'm still waiting for the day when I receive a phone call from a developer or operations person panicing because they've just deleted the production AppSync API and they can't re-create the old URL.

With web applications this is rarely a major issue because you can deploy an update with the new API endpoint quickly. It's a different story if you need to compile an Android/iOS application, publish the update in the app store and then wait for devices to update the software. Your users will probably experience an outage that lasts days.

The API Gateway has a solution to this with custom domains and I was really hoping that AppSync would support them by now.

With our latest application about to go into production I decided to experiment with putting an API Gateway in front of AppSync. The API Gateway supports custom domain names and I can configure it to proxy all requests to the AppSync API endpoint.

My first step in testing this was to setup an AppSync API using AWS Amplify CLI. It's overkill, and I'm not going to use the client, but it's also much faster than setting up an AppSync API through the AWS console. Once I had my AppSync API I sent it a few queries using RESTClient to confirm it was working correctly.

Next I created an API Gateway using SAM with a custom domain name. The definition for the API Gateway was written using the Open API spec. All incoming requests are proxied to the AppSync API using the API Gateway HTTP proxy integration.

Here's the template.yml for SAM.

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: AppSync custom domain template

Parameters:
  DomainName:
    Type: String
  Stage:
    Type: String
    Default: dev

Resources:
  ApiGw:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref Stage
      DefinitionBody:
        openapi: "3.0.0"
        info:
          version: 1.0.0
          title: AppSync custom domain
          description: AppSync custom domain
          contact:
            name: Richard Buggy
            email: rich@richdevelops.dev
            url: https://www.richdevelops.dev/
        servers:
          - url: https://api.richdevelops.dev
        paths:
          /{proxy+}:
            "x-amazon-apigateway-any-method":
              summary: "GraphQL Proxy"
              operationId: graphQL
              x-amazon-apigateway-integration:
                uri: https://XXXXXXXXXXXXXXXXXXXXXXXXXX.appsync-api.XXX-XXXXXX-X.amazonaws.com/{proxy}
                passthroughBehavior: when_no_match
                httpMethod: ANY
                type: http_proxy
      EndpointConfiguration: REGIONAL

  ApiGwCertificate:
    Type: AWS::CertificateManager::Certificate
    Properties:
      DomainName: !Ref DomainName
      ValidationMethod: DNS

  ApiGwDomainName:
    Type: AWS::ApiGateway::DomainName
    DependsOn:
      - ApiGwCertificate
    Properties:
      DomainName: !Ref DomainName
      EndpointConfiguration:
        Types:
          - REGIONAL
      RegionalCertificateArn: !Ref ApiGwCertificate

  ApiGwBasePathMapping:
    Type: AWS::ApiGateway::BasePathMapping
    DependsOn:
      - ApiGwDomainName
      - ApiGw
    Properties:
      DomainName: !Ref DomainName
      RestApiId: !Ref ApiGw
      Stage: !Ref Stage

Once the API Gateway was setup I used RESTClient to check it was proxying correctly to the AppSync API. Success!!

While it introduces extra latency and it isn't as convenient as adding a custom domain name directly to AppSync it does remove the worry that our endpoint will simply disappear one day due to human error. When AppSync introduces custom domain name support I'll remove the API Gateway and attach the domain name directly to the AppSync API.