DEV Community

Cover image for Configuring a Custom Domain for API Gateway with AWS Cloud Development Kit (CDK): SSL Certificate Use and Route 53 Integration
Kevin Lactio Kemta
Kevin Lactio Kemta

Posted on

Configuring a Custom Domain for API Gateway with AWS Cloud Development Kit (CDK): SSL Certificate Use and Route 53 Integration

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:

  1. 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.

  2. Update the Existing Stack
    Modify the existing stack by instantiating the new construct with the appropriate parameters.

  3. Deploy the New Infrastructure
    Deploy the updated infrastructure and update the endpoint values in the environment.ts file accordingly.

Diagram

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:

  1. 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>")
Enter fullscreen mode Exit fullscreen mode
  1. 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
})
Enter fullscreen mode Exit fullscreen mode
  • 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 using us-east-1 to create/import the certificate, we intend to create an edge-optimized API Gateway. For this type, the certificate must be created in us-east-1. If you choose to create a regional API Gateway, the certificate must reside in the region where the API Gateway is created.
  1. 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
})
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
  • 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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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!
    });
    ....
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/'
}
Enter fullscreen mode Exit fullscreen mode

__

πŸ₯³βœ¨
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)