At first, setting up an API Gateway is fairly straightforward. As your application grows, you have multiple services behind an API and you’re likely to need some sort of single domain to access all APIs. I have come across this scenario a few times. When working with microservices in a container environment, you might have a dedicated API Service with some intelligence (at least for authorization), a reverse proxy or load balancer. There are many solutions for this. With Serverless on AWS, the approach will be different.
AWS API Gateway
In an AWS-Serverless context, there is one go-to option – the AWS API Gateway. You could use an Application Load Balancer but this is typically for the case where you have non-serverless systems in the mix and want to serve all traffic through the same API endpoint and get load-balancing to containers or EC2 instances as part of the solution. This post is all about the API Gateway and how to set it up so that you can serve requests to multiple serverless “services”. A service is defined as an independently-deployable stack within your overall serverless application that deals with a single concern.
Let’s start with the simple case. For this example, I’ll use the scenario from SLIC Starter, the open-source serverless starter application we recently released on GitHub. The application in SLIC Starter is called SLIC Lists and it allows you to create and manage checklists. At first, we had a service called the Checklist Service. It has an API to create, read and edit checklists. It is implemented in AWS Lambda and backed by DynamoDB.
We are using the Serverless Framework to define and deploy this service. It deals with a lot of the boilerplate of creating all the CloudFormation resources that make up an API Gateway deployment. In the Serverless Framework configuration (serverless.yml), we define an http event for the Lambda function as follows.
get:
handler: services/checklists/get.main
events:
- http:
path: /{id}
method: get
The Serverless Framework will automatically provision the following set of resources to make that happen!
Once deployed, the important output of this API Gateway is the API Gateway endpoint URL. It looks something like this: https://ab7xyzpqr4.execute-api.eu-west-1.amazonaws.com/dev
The generated ID in this URL is the ID of the REST API resource for the API. For each application/stack you deploy with the Serverless Framework, this will be different. In order for API clients to invoke our API, we prefer to avoid a generated name like this. Using the generated URL means we would have to inject the URL into our front-end code somehow. If our API is redeployed, it has to be changed. I would rather address my API using a URL derived according to a known convention. This is why I always use a dedicated API domain, like api.sliclists.com to address the application’s API.
Using Custom API Domains
How do we set up our API Gateway to use a domain instead of the generated endpoint? This is achieved using an API Gateway Custom Domain Name. It can be created via CloudFormation (via SAM or Serverless Framework) and linked to the API Gateway via a Base Path Mapping. In the AWS Console, the Custom Domain Name configuration looks like this:
The configuration in CloudFormation (via serverless.yml) looks like this.
apiCustomDomain:
Type: AWS::ApiGateway::DomainName
Properties:
CertificateArn: ${self:custom.apiConfig.apiCert}
DomainName: api.sliclists.com
apiCustomDomainPathMappings:
Type: AWS::ApiGateway::BasePathMapping
Properties:
BasePath: ''
RestApiId:
Ref: ApiGatewayRestApi
DomainName:
Ref: apiCustomDomain
Stage: prod
apiDomainDns:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneId: ${self:custom.apiConfig.publicHostedZone}
RecordSets:
- Name: ${self:resources.Resources.apiCustomDomain.Properties.DomainName}
Type: A
AliasTarget:
DNSName: { Fn::GetAtt: [apiCustomDomain, DistributionDomainName] }
HostedZoneId: ${self:custom.cloudFrontHostedZoneId}
- Name: ${self:resources.Resources.apiCustomDomain.Properties.DomainName}
Type: AAAA
AliasTarget:
DNSName: { Fn::GetAtt: [apiCustomDomain, DistributionDomainName] }
HostedZoneId: ${self:custom.cloudFrontHostedZoneId}
The custom domain is created and linked to an AWS Certificate Manager certificate which has been provisioned separately for the domain. The next resource, the BasePathMapping, links our custom domain to the API Gateway created by the serverless framework. This resource is always given the name ApiGatewayRestApi. This configuration ensures that all requests ( denoted by the ‘/’ path) are mapped to the single API Gateway REST API for our service.
The last part of this single API setup is to create DNS entries for our domain that alias the API Gateway HTTP servers. We do this by creating A records (also AAAA for IPv6) that alias our API domain to the distribution domain name for the custom domain.
The value of this distribution domain name will be something like a1bcd9xyzovgag.cloudfront.net and is an output of the Custom Domain Name resource. We use Route53 for our DNS entries so all of this can be represented in a single CloudFormation stack.
When API requests are made to https://api.sliclists.com, they then follow this sequence:
- The client makes a DNS request for api.sliclists.com. A number of IP addresses for API Gateway (CloudFront) endpoints are returned.
- The client makes an HTTP request to one of these endpoints. It will include the Host header (Host: api.sliclists.com).
- The API Gateway (CloudFront) infrastructure will match the Host header to provisioned Custom Domain Names.
- The path in the request will be matched to the Base Path Mappings configured for the custom domain name. In our simple example, this will map the API REST API created by the Serverless Framework.
- The request will be passed to our API REST API. In turn, the lambda_proxy event handler will result in our Lambda function being invoked with the request payload. Add paragraph text here.
Moving to Multiple Services
We have outlined the configuration for one service. How does this expand to support multiple services as part of an overall application? For our SLIC Lists application, we dealt with this when we added a “sharing service” to handle requests to share lists with new users. This was deemed to be out of the scope of the checklists service.
The simple approach here is to add a second base path mapping. Instead of directing all requests to a single services, we have some routing:
- All requests starting with /checklist go to the checklist service’s REST API
- All requests starting with /share go to the sharing service’s REST API
The updated Custom Domain Name base path mapping looks like this:
This example shows the development environment. That’s why the stage is dev and a dedicated .dev subdomain is shown.
In our single-service case, everything could be provisioned within one service. When we want multiple APIs, this solution does not work or scale. Each service creates a separate API REST API in API Gateway but is required to share an API Custom Domain. For SLIC Starter, we separate our deployments into four separate packages to provide these two services. The four services are:
- Certs – deploys the AWS Certificate Manager resources and Route53 hosted zone for the domain. This must be deployed separately since these certificates must be deployed into us-east-1 regardless of the region used in the rest of the application. Any certificate used by API Gateway or CloudFront must be deployed in us-east-1 as that’s where the service endpoint for CloudFront is located.
- API Service – deploys the custom domain name (e.g. api.sliclists.com) and its associated Route53 DNS records (Record Sets)
- Checklist Service – creates its own API Gateway REST API and a Base Path Mapping for api.sliclists.com/checklist to this API Gateway
- Sharing Service – creates its own API Gateway REST API and a Base Path Mapping for api.sliclists.com/share to this API Gateway
The sharing service and checklist service do not have to look up any reference to the custom domain name created in the API service. Thankfully, this link is established by providing the domain name in the base path mapping alone.
NOTE! There is an important caveat when doing this with the Serverless Framework. A known issue sometimes results in a dependency issue when creating the base path mapping. To work around the issue, a dummy “AWS::ApiGateway::Deployment” resource must be created.This is unfortunate! The issue is here and the workaround can be seen in the SLIC Starter project here.
Putting it altogether, the picture of these four services and the resources they create looks like this:
You can see the full code for this in the SLIC Starter project. Take a look in particular at api-service/serverless.yml, checklist-service/serverless.yml and certs/serverless.yml. Special thanks to Paul Kevany for implementing this in SLIC Starter and for reviewing this post.
If you have any questions, feel free to create an issue on GitHub or reach me on Twitter – @eoins.
Get in touch to share your thoughts.