Day 010 - 100DaysAWSIaCDevopsChallenge
Recently in the Day 009 of my 100 Days of Code Challenge, I created and deployed an infrastructure for securing API Gateway methods using AWS Cognito. Since these APIs are used by an Angular app, it is necessary to update the environment.ts
file with the new backend endpoint after every API Gateway ID renewest. To avoid this repetitive update with each infrastructure modification, today I am going to configure a domain name in front of our API Gateway, so that we no longer need to change the environment.ts
file for every API Gateway destruction.
To achieve this, we will follow these steps:
Set Up a New CDK Construct for Domain Name Configuration
Create a new CDK construct to handle the domain name configuration for the API Gateway.Update the Existing Stack
Modify the existing stack by instantiating the new construct with the appropriate parameters.Deploy the New Infrastructure
Deploy the updated infrastructure and update the endpoint values in the environment.ts file accordingly.
Set Up a New CDK Construct for Domain Name Configuration
To make the infrastructure stack more readable and maintainable, I prefer to create a custom construct for setting up a domain name for the API Gateway. This is necessary because creating a domain name for the API Gateway involves configuring multiple AWS resources. Within this custom construct, the following resources will need to be configured:
-
Retrieve Certificate by ARN
To customize a domain name for our API Gateway, AWS requires us to prove ownership of the domain. The SSL/TLS certificate, typically provided by AWS Certificate Manager (ACM), verifies that we control the domain associated with the custom API Gateway domain name. Since I have already requested my certificate in
ACM
, the following code snippet retrieves the existing certificate:
const existingCertificate = acm.Certificate.fromCertificateArn( this,
`ApiDomainCertificate_${id}`,
"<CERTIFICATE_ARN>")
- The Custom domain name
// const props: {api: IRestApi, apiDomain: string} = {...}
const domain = new api.DomainName(this, `CustomApiDomainName`, {
domainName: props.apiDomain,
certificate: existingCertificate,
endpointType: api.EndpointType.EDGE,
securityPolicy: api.SecurityPolicy.TLS_1_2,
mapping: props.api
})
-
domainName
- The domain name to be used. In my case, it is a subdomain like todo-api.mydomain.com. -
certificate
- The previously retrieved certificate, looked up by its ARN. -
mapping
- The API Gateway that will be mapped to the domain name. -
endpointType
- Since we are usingus-east-1
to create/import the certificate, we intend to create an edge-optimized API Gateway. For this type, the certificate must be created inus-east-1
. If you choose to create a regional API Gateway, the certificate must reside in the region where the API Gateway is created.
- Hosted Zone & RecordSet
Since the hosted zone has already been created, we just need to find it by using the main domain name.
// const props: {api: IRestApi, apiDomain: string, domain: string} = {...}
const hostedZone = route53.HostedZone.fromLookup(this,
`HostedZoneLookup_${generateResourceID()}`, {
domainName: props.domain
})
Then, create an A
record for the subdomain that will route all traffic to the API Gateway domain name.
// const props: {api: IRestApi, apiDomain: string, domain: string} = {...}
const a_record = new route53.ARecord(this, `A_Record`, {
recordName: props.apiDomain.concat('.'),
zone: hostedZone,
target: route53.RecordTarget.fromAlias(new targets.ApiGatewayDomain(domain)),
ttl: Duration.minutes(2),
comment: `The Record “A“ to route traffic from subdomain ${props.apiDomain} to the api gateway #${props.api.restApiId}`,
region: props.env?.region,
deleteExisting: true
})
a_record.applyRemovalPolicy(RemovalPolicy.DESTROY)
-
recordName
- The subdomain name for this record. Note that the value ends with a dot (.
). If you don't include the dot, CDK will treat it as a segment rather than a fully qualified domain name.
recordName | result |
---|---|
foo | foo.mydomain.com |
zuzu.mydomain.com | zuzu.mydomain.com.mydomain.com |
bar.mydomain.com. | bar.mydomain.com |
The full code:
import { Construct } from 'constructs'
import { CustomStackProps } from '../custom-stack.props'
import {
aws_apigateway as api,
aws_certificatemanager as acm,
aws_route53 as route53,
aws_route53_targets as targets,
Duration,
RemovalPolicy
} from 'aws-cdk-lib'
import { generateResourceID } from './utils'
interface ApiDomainNameProps extends CustomStackProps {
apiDomain: string,
api: api.RestApi,
certificateArn: string
}
export class ApiDomainName extends Construct {
constructor(scope: Construct, id: string, private props: ApiDomainNameProps) {
super(scope, id)
if (!props.domain) {
throw new Error('The domain parameter must be specified as the ApiDomainName has been instantiated')
}
if (!this.props.apiDomain.endsWith(props.domain)) {
throw new Error(`The apiDomain parameter must be a subdomain of ${this.props.domain}`)
}
const existingCertificate = acm.Certificate.fromCertificateArn(this, `ApiDomainCertificate_${id}`, props.certificateArn)
const domain = new api.DomainName(this, `CustomApiDomainName_${id}`, {
domainName: props.apiDomain,
certificate: existingCertificate,
endpointType: api.EndpointType.EDGE,
securityPolicy: api.SecurityPolicy.TLS_1_2,
mapping: props.api
})
domain.node.addDependency(props.api)
const hostedZone = route53.HostedZone.fromLookup(this, `HostedZoneLookup_${generateResourceID()}`, {
domainName: props.domain
})
const a_record = new route53.ARecord(this, `A_Record_${id}`, {
recordName: props.apiDomain.concat('.'),
zone: hostedZone,
target: route53.RecordTarget.fromAlias(new targets.ApiGatewayDomain(domain)),
ttl: Duration.minutes(2),
comment: `The "A" record is used to route traffic from the subdomain ${props.apiDomain} to the API Gateway #${props.api.restApiId}`,
region: props.env?.region,
deleteExisting: true
})
a_record.applyRemovalPolicy(RemovalPolicy.DESTROY)
}
}
Update the Existing Stack
export class CdkStack extends BaseStack {
constructor(scope: Construct, id: string, private props: CustomStackProps) {
....
new ApiDomainName(this, 'TodoListApiDomainName', {
...props,
apiDomain: props.route53?.apigw?.subdomain ?? 'api.' + this.props.domain,
api: restApi,
certificateArn: props.route53?.apigw?.certificateArn!
});
....
}
}
Deploy the entire infrastructure
git clone https://github.com/nivekalara237/100DaysTerraformAWSDevops.git
cd 100DaysTerraformAWSDevops/day_010
export STAGE_NAME="dev" # needed by api gateway staging
export CERTIFICATE_ARN="arn:aws:acm:us-east-1:xxxx:certificate/xxxx"
cdk deploy --profile cdk-user --all
Re-deploy app as S3 static website
To deploy the Angular app to the S3 bucket as a website, follow the instructions in my previous article 👉🏽👉🏽 Deploying a REST API and Angular Frontend Using AWS CDK, S3, and API Gateway↗
⚠️⚠️ Make sure to update the API Gateway URL in the src/environment.ts file with the correct API Gateway endpoint before building and deploying the application.
export const environment = {
production: true,
apiUrl: 'https://todo-api.mydomain.com/'
}
__
🥳✨
We have reached the end of the article.
Thank you so much 🙂
Your can find the full source code on GitHub Repo↗
Top comments (0)