AWS API Gateway with a Custom Domain Using AWS CDK

AWS API Gateway with a Custom Domain Using AWS CDK

In this blog post, we'll illustrate how CDK can be utilized to provision code for execution within a Lambda function, integrated with API Gateway. We'll then deploy this setup with a custom domain, leveraging Certificate Manager and your chosen DNS registrar, thereby circumventing Route 53 configuration and associated charges.

CDK, or the Cloud Development Kit, is a powerful tool that allows you to define and deploy AWS cloud infrastructure using programming languages like TypeScript, Python, and Java.

Since we’re not utilising Route 53, I recommend setting up an SSL certificate outside of the CDK stack. We’ll use it’s reference ARN later.

Requesting a Certificate in AWS Console:

  1. Sign in to the AWS Management Console and access your account.
  2. Open AWS Certificate Manager (ACM)
  3. Request a Certificate by clicking on the “Request” button.
  4. Enter Domain Name and click “Request”.
  5. Open Certificate from the list, and Store CNAME Details: Note the CNAME name and values provided.
  6. Adding a DNS Record:
  7. Log in to your domain registrar’s website.
  8. Go to the DNS settings or management section.
  9. Add a CNAME record: Use the AWS-provided name in the “Name” or “Host” field and the CNAME value in the “Value” field.
  10. Save the changes.

Awaiting Validation:

  1. Return to the AWS ACM console.
  2. Refresh the page to check the validation status of your certificate. It might take a while.
  3. Once validated and the certificate status changes to “Issued,” you are not ready to use the certificate.

Create a CDK Project for your API

1$ mkdir my-aws-api 2$ cd my-aws-api 3$ cdk init app --language typescript

CDK Stack

A CDK Stack(represented here by the Stack Class) is a collection of AWS resources to be provisioned.

1 2import * as cdk from 'aws-cdk-lib'; 3import { Construct } from 'constructs'; 4import {aws_certificatemanager as acm} from 'aws-cdk-lib'; 5 6export class ApiCustomDomainStack extends cdk.Stack { 7 constructor(scope: Construct, id: string, props?: cdk.StackProps) { 8 super(scope, id, props); 9 10 const DOMAINName = 'api.example.com'; 11 const SSL_CERTIFICATE_ARN = "arn:aws:acm:af-south-1:123456788000:certificate/12abcde1-1200-3400-5600-d12c12345678" 12 const API_STAGE_NAME = 'dev'; 13 14 const certificate = acm.Certificate.fromCertificateArn(this, 'Certificate', SSL_CERTIFICATE_ARN); 15 16 } 17}

In the first few lines of the constructor we will specify some constants and references that will be used later

Add an API to the stack

The first thing we’ll do is add an API.

1import {aws_apigateway as apigateway} from 'aws-cdk-lib'; 2 3... 4 5const api = new apigateway.RestApi(this, 'MyApi', { 6 restApiName: 'My API', 7 deployOptions: { 8 stageName: API_STAGE_NAME, 9 } 10});

Here we are creating a new REST API using AWS API Gateway. The restApiName is used so that we can identify it in the AWS Console. and the stageName is to specify the deployment stage of the API(e.g. dev, testing, staging, production)

Add a custom domain to the API

We always want to link our API to a custom domain. This can help ensure consistency in how your API is accessed, even if you switch backend services.

1const domain = new apigateway.DomainName(this, 'CustomDomainName', { 2 domainName: DOMAINName, 3 certificate 4}); 5 6new apigateway.BasePathMapping(this, 'ApiMapping', { 7 domainName: domain, 8 restApi: api, 9})

Here we are setting up API Gateway custom domain name with the SSL certificate. We are then required to map the API we’ve created in the previous step to the custom domain name.

Securing the API with an API Key

API keys are essential for managing and securing API access, They uniquely identify consumers of the API. While not foolproof, API keys contribute an additional layer of security. API keys also help with monitoring usage patterns and control rate limits

1const plan = new apigateway.UsagePlan(this, 'MyUsagePlan', { 2 apiStages: [ 3 { 4 api: api, 5 stage: api.deploymentStage, 6 }, 7 ], 8}); 9 10const key = new apigateway.ApiKey(this, 'MyApiKey', { 11 description: 'API key' 12}); 13 14plan.addApiKey(key); 15

Because API keys enable us to manage access and impose rate limits, we must first create a usage plan and then associate the key with this plan. Later, we will demonstrate how to select specific endpoints to secure.

API Functionality

Besides configuring the API, we can also define the specific functionality each API endpoint should offer by using AWS Lambda.

1const myLambda = new lambda.Function(this, 'MyLambda', { 2 runtime: lambda.Runtime.NODEJS_LATEST, 3 handler: 'index.handler', 4 code: lambda.Code.fromAsset('lambda'), 5}); 6 7 8const integration = new apigateway.LambdaIntegration(myLambda); 9 10api.root.addMethod('GET', integration); 11 12api.root.addResource('hello').addMethod('GET', integration, { 13 apiKeyRequired: true 14})

This code configures an AWS Lambda function and links it to an API Gateway endpoint. It creates two endpoints: one for GET requests to the root URL (/) and another for GET requests to /hello. The /hello endpoint is secured with an API key, ensuring controlled access.

Below is an example of a lambda function

1// lambda/index.mjs 2 3export const handler = async (event) => { 4 return { 5 statusCode: 200, 6 body: JSON.stringify({ 7 message: 'Hello from Lambda!' 8 }, null, 2), 9 }; 10};

Retrieving the Information

During the CDK deployment process, we can configure CDK to output certain values:

1new cdk.CfnOutput(this, ‘Api Key ID, { 2 value: key.keyId, 3}); 4 5new cdk.CfnOutput(this,API Gateway Domain Name’, { 6 value: domain.domainNameAliasDomainName, 7});

This code will generate output values for the API Gateway’s CNAME associated with our custom domain and the API Key ID. Note that it outputs the API Key ID, not the actual key. To retrieve the full API key value from the command line, use the following AWS CLI command:

1$ aws apigateway get-api-key — api-key <API_KEY_ID> — include-value

Replace <API_KEY_ID> with the key ID obtained from the CDK deployment output. Once retrieved it can be used in the header follows

1$ curl -X GET "https://api.example.com/hello" -H "x-api-key:<API_KEY>"

Complete code

1import * as cdk from 'aws-cdk-lib'; 2import { Construct } from 'constructs'; 3import {aws_lambda as lambda} from 'aws-cdk-lib'; 4import {aws_certificatemanager as acm} from 'aws-cdk-lib'; 5import {aws_apigateway as apigateway} from 'aws-cdk-lib'; 6 7export class ApiCustomDomainStack extends cdk.Stack { 8 constructor(scope: Construct, id: string, props?: cdk.StackProps) { 9 super(scope, id, props); 10 11 const DOMAINName = 'api.example.com'; 12 const SSL_CERTIFICATE_ARN = "arn:aws:acm:af-south-1:123456788000:certificate/12abcde1-1200-3400-5600-d12c12345678" 13 const API_STAGE_NAME = 'dev'; 14 15 const certificate = acm.Certificate.fromCertificateArn(this, 'Certificate', SSL_CERTIFICATE_ARN); 16 17 const api = new apigateway.RestApi(this, 'MyApi', { 18 restApiName: 'My API', 19 deployOptions: { 20 stageName: API_STAGE_NAME, 21 } 22 }); 23 24 const domain = new apigateway.DomainName(this, 'CustomDomainName', { 25 domainName: DOMAINName, 26 certificate 27 }); 28 29 new apigateway.BasePathMapping(this, 'ApiMapping', { 30 domainName: domain, 31 restApi: api, 32 }) 33 34 const plan = new apigateway.UsagePlan(this, 'MyUsagePlan', { 35 apiStages: [ 36 { 37 api: api, 38 stage: api.deploymentStage, 39 }, 40 ], 41 }); 42 43 const key = new apigateway.ApiKey(this, 'MyApiKey', { 44 description: 'API key' 45 }); 46 47 plan.addApiKey(key); 48 49 const myLambda = new lambda.Function(this, 'MyLambda', { 50 runtime: lambda.Runtime.NODEJS_LATEST, 51 handler: 'index.handler', 52 code: lambda.Code.fromAsset('lambda'), 53 }); 54 55 const integration = new apigateway.LambdaIntegration(myLambda); 56 57 api.root.addMethod('GET', integration); 58 59 api.root.addResource('hello').addMethod('GET', integration, { 60 apiKeyRequired: true, 61 }) 62 63 64 new cdk.CfnOutput(this, 'Api Key ID', { 65 value: key.keyId, 66 }); 67 68 new cdk.CfnOutput(this, 'API Gateway Domain Name', { 69 value: domain.domainNameAliasDomainName, 70 }); 71 72 } 73} 74

Deploy

We can now deploy the stack

1$ cdk deploy

Finally

Add a CNAME DNS record for the custom domain using the CDN domain name, follow these steps:

Updating DNS Settings at Your Domain Registrar:

  1. Sign in to your domain registrar’s website.
  2. Go to the DNS settings or management section.
  3. Add a DNS Record.
    • Choose Record Type: CNAME record:
    • Input your domain name (e.g., api.example.com). *Input the CNAME value provided by the AWS API Gateway output ( eg, d-1h1abc12ab.execute-api.af-south-1.amazonaws.com).
    • Save Changes

Once the DNS changes have propagated, your custom domain will be successfully mapped.