A common requirement for [[Serverless MOC|Serverless]] projects is to require your [[AWS API Gateway|API Gateway]] endpoint or your [[AWS CloudFront|CloudFront]] distribution to be accessed via a custom "friendly" domain. This requires creation of 2 resource types:
- [[AWS Route53|Route53]] HostedZone (and associated A record)
- [[AWS ACM|ACM]] Certificate
These resources can be created in [[Infrastructure-as-Code]] using [[AWS CloudFormation]], or an abstraction over CF such as [[Serverless Framework]] (which I'll use here).
## Why use a custom domain at all for APIs?
A custom domain means than any clients referencing an API endpoint isn't tightly coupled to the specific instance of an API Gateway or CloudFront distribution created at the backend, which they would be if they used the auto-generated domains for each service.
## Why use a standalone stack?
You could just include these resources in the same stack that you use for your application resources (API Gateway config, Lambda functions, etc), but there are a few reasons why it's worthwhile extracting them into their own stack:
1. Certificates may be shared across multiple dev teams working on the same product (e.g. a front-end web team and a back-end API team). You don't want one team being dependent on resources in another's application stack.
2. For security reasons, certain organisations require that public DNS and SSL certs can only be provisioned by a centralised "platform" team and not by application developer teams. Therefore you wouldn't want to code these resources into your application stack since your app devs won't be able to deploy them.
## Environment-specific subdomain specification
Let's assume we have 3 stages (environments),`dev`, `test` and `prod`, where we wish to deploy an API Gateway endpoint and a CloudFront distribution to serve static assets for a web app. We also own the domain `exampleapp.com`.
Here's a common subdomain design:
| Stage| HostedZone Name | API | Web App |
|- |- |- |- |
| `dev` | `dev.exampleapp.com`| `api.dev.exampleapp.com` | `app.dev.exampleapp.com` |
| `test` | `test.exampleapp.com` | `api.test.exampleapp.com` | `app.test.exampleapp.com` |
| `prod` | `prod.exampleapp.com` | `api.prod.exampleapp.com` | `app.prod.exampleapp.com` |
Note that in the `prod` stage, we may not want to have the stage included as part of the subdomain at all (see [[How to configure environment-specific DNS and SSL certificates in AWS using IaC#OpenQuestions]] below).
## Implementation steps
### Pre-requisites
- A HostedZone has been created for the root `exampleapp.com` domain. If you're using a multi-account setup, this is often created in a "Tools" account and not an account specific to any environment.
- HostedZones have been created in the AWS account for each stage, and DNS has been delegated to each zone from the root zone (see [[How to delegate DNS for subdomains to a different Route53 HostedZone]])
### [[Serverless Framework]] configuration
The following `serverless.yml` file creates a stack with a HostedZone and an SSL cert which uses a wildcard to support the environment-specific subdomain and any subdomains nested beneath it.
Notice that the IDs and ARNs are marked as outputs so they can then be referenced from other stacks.
```yml
# serverless.yml
service: ${self:custom.appName}-dns
provider:
name: aws
region: eu-west-1
runtime: nodejs12.x
stage: ${opt:stage,'dev'}
logRetentionInDays: 30
custom:
appName: exampleapp #used to prefix stack resources
dnsRootDomain: exampleapp.net # root domain that all environments will be under
currentStageDnsSubdomain: ${self:provider.stage}.${self:custom.dnsRootDomain}
resources:
Resources:
HostedZone:
Type: AWS::Route53::HostedZone
Properties:
Name: ${self:custom.currentStageDnsSubdomain}
HostedZoneConfig:
Comment: "Hosted zone for project '${self:custom.appName}' in stage '${self:provider.stage}'"
SslCertificate:
Type: AWS::CertificateManager::Certificate
Properties:
DomainName: '*.${self:custom.currentStageDnsSubdomain}' # wildcard for sub-subdomains (app, api, etc)
SubjectAlternativeNames:
- '${self:custom.currentStageDnsSubdomain}' # apex domain
ValidationMethod: DNS
DomainValidationOptions:
- DomainName: '*.${self:custom.currentStageDnsSubdomain}'
HostedZoneId: !Ref HostedZone
Outputs:
HostedZoneId:
Value: !Ref HostedZone
HostedZoneDomainName:
Value: '${self:custom.currentStageDnsSubdomain}'
SslCertificateArn:
Value: !Ref SslCertificate
```
Use the `npx sls deploy --stage dev` command to deploy this to your dev account.
## Validating the ACM certificate
When the above configuration is deployed, CloudFormation creates CNAME DNS records inside the new HostedZone in order to prove that you own the domain for which you are creating the certificate, using a UUID provided by ACM as a secret.
ACM will now attempt to perform polling DNS queries until it finds the CNAME record containing the UUID secret. While this is proceeding, the CloudFormation stack remains in the UPDATE_IN_PROGRESS state as ACM intermittently polls the DNS.
However, as things stand, this will never succeed because the subdomain for the newly created HostedZone is not yet wired up to the root HostedZone. So the CloudFormation deploy will eventually timeout and fail.
To prevent this, follow the steps here to delegate the DNS from the root-level HostedZone (created in the pre-reqs): [[How to delegate DNS for subdomains to a different Route53 HostedZone]].
Once you have delegated the DNS to your HostedZone, the CloudFormation stack's update should eventually succeed.
## #OpenQuestions
- For production accounts, you often don't want to include the stage as part of the subdomain at all (e.g. `app.prod.exampleapp.com` should really be `app.exampleapp.com`). What's the best way to implement this in a multi-account setup, given that the top-level `exampleapp.com` HostedZone probably exists in a Tools account? Can it simply be achieved by creating a CNAME in the root HostedZone, e.g. `app.exampleapp.com` => `app.prod.exampleapp.com`? We would also need this to be included in the SubjectAlternateNames of the SSL cert. How would this affect the automated DomainValidation that ACM+CloudFormation performs?