This solution demonstrates how to create a Sub-Domain setup for Mult-Environment Infrascture, using CDK in Typescript. The solution accompanies this blog post from the I Love My Local Farmer Engineering Blog Series.
This README.md contains only architecture and installation steps. For more details, please check the blog post
- Minimum of 2 AWS accounts (one for root domain stack and atleast one for sub domain stack)
- A registered domain name
- AWS CLI v2: with AWS credentials configured (See steps here)
- AWS CDK (v. 2.2.0 or higher): for infrastructure and code deployment (See installation steps here
update cdk.json
and replace placeholders for the following:
- account number and region for each environment
- domain name
for the root account deploy the RootDomainStack
npm run deploy RootDomainStack
for the all other accounts (e.g. dev, staring & prod) deploy the SubDomainStack
npm run deploy SubDomainStack
The infrascture will deploy the following DNS delegation setup:
This is how our CDK code will be structured: we will split our Stack into 2 types, first one is the RootDomain Stack which will be deployed to our main root environment and second one is the SubDomain Stack which is re-used and deployed to the rest of our environments (dev, staging & prod)
The RootDomain Stack
This Stack consists mainly of the following Constructs:
- HostedZone responsible for the root domain
- UpdateRegDomain Custom Resource
- 3 HostedZones for each subdomain ( dev, staging & prod )
The root hosted zone is responsible for the root domain ilovemylocalfarmer.com. but this hosted zone will have a list of name servers that don’t match the ones in the registered domain (since we deleted the old hosted zone earlier, remember!). so we created a custom resource to update the registrar with the name servers of our new Hosted Zone. so what is a Custom Resource ? It is a customisable resource that you can put any code logic in a serverless function, which then is encapsulated as a standalone resource. This gives us the opportunity to add any customised step that is not available through any of the CDK assets available. So what we did is created a function that calls one of the AWS api UpdateDomainNameservers to update the registrar.
The remaining hosted zones are responsible for each subdomain. Each one of them will need to create a NS Record in the root hosted zone. so for example when a request comes in for dev.ilovemylocalfarmer.com to the root hosted zone, it won’t know what to do with it. so we need to inform the root hosted zone on how it can reach dev, hence we add the NS record to point to dev hosted zone. This is done for each subdomain hosted zone using the CDK construct ZoneDelegationRecord ( Constructs are just CDK building blocks that will have logic to create or modify resources ).
This stack will give us an IAM role called crossAccountZoneDelegationRole for each hosted zone which we will share to each subdomain stack. the purpose of this role is to give other accounts the permission to add records in their corresponding HostedZone. so for example dev.ilovemylocalfarmer.com
in the dev account can share their name servers by adding a NS Record in the Dev HostedZone. so that when a dns request comes in looking for dev subdomain, we know where to point it to.
So basically we have 4 IAM roles:
- dev delegation role: only allowed to update dev hosted zone for
dev.ilovemylocalfarmer.com
- staging delegation role: only allowed to update staging hosted zone for
staging.ilovemylocalfarmer.com
- prod delegation role: only allowed to update dev hosted zone for
prod.ilovemylocalfarmer.com
- root delegation role: this only allows to update the root hosted zone for ilovemylocalfarmer.com, this will only be given to prod, since only prod will have the the root domain be redirected to it.
Here is a diagram showing what our Root account looks like with our RootDomainStack deployed.
You can see for example all of the records created in the Hosted Zone. The first NS record is for the root domain, which our CustomResource will copy to the Registrar on the left as mentioned. we will go through the purpose of the remaining records in the next Stack, so please bear with me if they don’t make sense yet.
The SubDomain Stack
This Stack consists of the following Constructs:
- Certificate Custom Resource
- App - simple example of a Frontend (CloudFront, s3 Bucket) & Backend (ApiGw & lambda)
- AliasRecord Custom Resource
The same stack is used for each environment (dev, staging & prod ).
Certificate Custom Resource
CDK currently provides a construct called DNSValidatedCertificate. which creates a certificate for your domain e.g dev.ilovemylocalfarmer.com
and validates it by adding a validation CNAME record to your hosted zone proving you are the owner of this domain name. This would have been perfect and straight forward for all our environments and their subdomains. However we had one big problem we faced. This CDK Construct doesn’t provide any option to validate certificate via DNS if the hosted zone for the domain name is in another account. so this would work fine if we are creating a certificate for dev.ilovemylocalfarmer.com
as it’s hosted zone is in our dev account. But for an account like prod, where we are planning to associate the root domain (localfarmer.com) as well, we need to update the hosted zone that lives in our Root account. And since DNSValidatedCertificate doesn’t give us this capability. we had to create our own custom resource that gives us this flexibility.
The custom resource contains mainly 2 pieces of logic. first one is the eventHandler that just requests for a new certificate with the domain names. second one is a validation completion handler that does a few things:
- It waits till certificate has generated the validation details
- Creates CNAME records with the validation details
- Waits till certificate has been validated to confirm that you are the owner of the domain
In the prod environment it adds an extra step where it will update the root hostedZone in the rootDomain account to also validate that you are the owner of root domain ilovemylocalfarmer.com
.
So now that we have this nice construct we will create 2 certificates. one for cloudfront and the other for our api endpoint since we are planning to use a custom api domain. e.g for dev it would be api.dev.ilovemylocalfarmer.com
.
You might be wondering why 2 certificates ? why not have both domains in one certificate and reuse it for both ? Well that was our initial intention. but we came across this nice puzzle that AWS required us to solve. Cloudfront only accepts certificates in us-east-1 region and ApiGw only accepts certificates in same region it is in. Since our infrastructure is deployed in eu-west-1, we are forced to create 2 certificates to satisfy both requirements.
App
This construct just creates a cloudfront & s3 bucket to hold frontend assets and an ApiGw & Lambda for a simple backend api. Cloudfront in dev will use subdomain dev.ilovemylocalfarmer.com
and apiGW will use api.dev.ilovemylocalfarmer.com
. and if we are in prod then cloudfront will also add root domain ilovemylocalfarmer.com
.
RootAliasRecord Custom Resource
After the cloudfront and ApiGw are created, this Custom Resource adds an Alias record in the corresponding hosted zone to point the subdomain to it. e.g dev.ilovemylocalfarmer.com
to point to cloudfront-domain.com
or api.dev.ilovemylocalfarmer.com
to api-gateway-endpoint.com
. And you guessed it, it uses the same delegation role passed in from the Root Stack.