Home API Gateway Add Base Path Mapping into Existing Custom Domain with AWS CDK
Post
Cancel

API Gateway Add Base Path Mapping into Existing Custom Domain with AWS CDK

In this tutorial we will learn how to add base path mappings into an existing custom domain on API Gateway. Managing shared infrastructure in a separate stack is one of the most common ways in software world. We generally create this stack for provisioning resources like databases, VPCs, domains etc. which are most likely to be referenced from another stacks belonging to different teams. And one day I was searching a way to apply this same flow for API Gateway to make it extensible for adding multiple base path mappings into same custom domain which I struggled with a few days. However, I am not sure if this is a good practice. Anyway, to illustrate this approach we will have two different stacks;

  • InfrastructureStack: To create a new Certificate for our domain, DomainName for generating an alias target for API Gateway and an ARecord to make Route53 map inbound requests to the generated API Gateway domain.
  • AppStack: To import the DomainName which will be provided when creating the BasePathMapping.

Infrastructure Stack

We will proceed by provisioning resources step by step in this stack to clearly understand what happens behind the scenes.

Firstly, we will import the HostedZone by our domain name (the value itself is imported from cdk context). Keep in mind that, I am using a domain registered from Route53 not a domain that created from another registrar. And I am getting

1
2
3
4
5
// get hard-coded domainName from cdk context
const stackProps = this.node.tryGetContext('stackProps');

// Import existing hosted zone (registered domain)
const hostedZone = route53.HostedZone.fromLookup(this, 'hosted-zone', { domainName: stackProps.domainName });

Now we have our hostedZone construct in our hand and the next step will be creating a Certificate which will be auto-approved since we are using a domain created and managed from Route53 itself. Otherwise, there are a few validation types that we need to take manual actions and cdk deploy won’t proceed until it is validated.

1
2
3
4
5
// create a new certificate which will be auto approved since we own the domain
const certificate = new acm.Certificate(this, 'main-domain-certificate', {
  domainName: stackProps.domainName,
  validation: acm.CertificateValidation.fromDns(hostedZone),
});

We will assign this created certificate to our existing custom domain via DomainName construct.

1
2
3
4
5
6
7
// create an API Gateway custom domain which will be added into ARecord
const domainName = new apigw.DomainName(this, 'domain-name', {
  domainName: stackProps.domainName,
  certificate: certificate,
  endpointType: apigw.EndpointType.REGIONAL,
  securityPolicy: apigw.SecurityPolicy.TLS_1_2,
});

Now if we deploy this infrastructure stack at this point then we will see 2 resources created with behaviours;

  • Certificate is attached to our domain.
  • A custom API Gateway domain is created without any mapping into it since there is no any associated alias record existing from our HostedZone’s DNS record set yet.

Verify that a new Certificate created and assigned to our domain.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
> aws acm list-certificates --region us-east-1

{
  "CertificateSummaryList": [{
    "CertificateArn": "arn:aws:acm:us-east-1:************:certificate/6d567b47-32b5-4f59-925b-3602e466e290",
    "DomainName": "awesome-cdk-examples.com",
    "SubjectAlternativeNameSummaries": [
      "awesome-cdk-examples.com"
    ],
    "HasAdditionalSubjectAlternativeNames": false,
    "Status": "ISSUED",
    "Type": "AMAZON_ISSUED",
    "KeyAlgorithm": "RSA-2048",
    "KeyUsages": [
      "DIGITAL_SIGNATURE",
      "KEY_ENCIPHERMENT"
    ],
    "ExtendedKeyUsages": [
      "TLS_WEB_SERVER_AUTHENTICATION",
      "TLS_WEB_CLIENT_AUTHENTICATION"
    ],
    "InUse": true,
    "RenewalEligibility": "ELIGIBLE",
    "NotBefore": "2023-03-21T03:00:00+03:00",
    "NotAfter": "2024-04-19T02:59:59+03:00",
    "CreatedAt": "2023-03-21T19:33:48.129000+03:00",
    "IssuedAt": "2023-03-21T19:34:18.147000+03:00"
  }]
}


An API Gateway custom domain is created with neither having any base path mappings and mapped from any alias (ARecord) records.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> aws apigatewayv2 get-domain-names --region us-east-1

{
  "Items": [{
    "ApiMappingSelectionExpression": "$request.basepath",
    "DomainName": "awesome-cdk-examples.com",
    "DomainNameConfigurations": [{
      "ApiGatewayDomainName": "d-ih6y4cxj26.execute-api.us-east-1.amazonaws.com",
      "CertificateArn": "arn:aws:acm:us-east-1:************:certificate/6d567b47-32b5-4f59-925b-3602e466e290",
      "DomainNameStatus": "AVAILABLE",
      "EndpointType": "REGIONAL",
      "HostedZoneId": "Z1UJRXOUMOOFQ8", // This is the hostedZoneId generated for 'ApiGatewayDomainName' NOT from our main domain
      "SecurityPolicy": "TLS_1_2"
    }]
  }]


1
2
3
> aws route53 list-resource-record-sets --hosted-zone-id Z04491901AJOYW36491VV --query "ResourceRecordSets[?Type == 'A']

[]


Alright, we are now moving forward with adding a new alias (ARecord) record to make Route53 point to the newly created API Gateway custom domain which is d-ih6y4cxj26.execute-api.us-east-1.amazonaws.com.

1
2
3
4
5
6
7
8
9
10
11
12
// add a new ARecord into our hosted zone DNS records to point API Gateway domain
const aRecord = new route53.ARecord(this, 'domain-name-arecord', {
  zone: hostedZone,
  recordName: stackProps.domainName,
  target: route53.RecordTarget.fromAlias(new targets.ApiGatewayDomain(domainName)),
});

new cdk.CfnOutput(this, 'domain-name-alias-domain-name', {
  value: domainName.domainNameAliasDomainName,
  description: 'alias domain name of the domain name',
  exportName: 'domainNameAliasDomainName',
});

Note that, target from ARecord is an actual construct which expects DomainName construct to be provided. Our Route53 is now configured to map inbound requests to API Gateway domain with created new A alias record.

1
2
3
4
5
6
7
8
9
10
11
> aws route53 list-resource-record-sets --hosted-zone-id Z04491901AJOYW36491VV --query "ResourceRecordSets[?Type == 'A']

[{
  "Name": "awesome-cdk-examples.com.",
  "Type": "A",
  "AliasTarget": {
    "HostedZoneId": "Z1UJRXOUMOOFQ8",
    "DNSName": "d-ih6y4cxj26.execute-api.us-east-1.amazonaws.com.",
    "EvaluateTargetHealth": false
  }
}]

As the final step, we have to export domainName.domainNameAliasDomainName value which will be assigned to domainNameAliasTarget property when importing DomainName from our AppStack. This was the place creating confusion to me because there wasn’t any place that AWS clearly mentions this in their docs. Instead, from their CDK DomainName documentation there is a description stating that domainName.domainNameAliasDomainName is a Route53 alias target in order to connect a record set to this domain through an alias. This is exactly the same description used for domainNameAliasTarget from DomainNameAttributes.


App Stack

We have everything ready for provisioning our api by adding a new base path mapping. We just need to import our API Gateway domain which is a shared resource. There won’t be any incremental steps we will follow to deploy instead we will just deploy our stack as a whole.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import * as cdk from 'aws-cdk-lib';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as apigw from 'aws-cdk-lib/aws-apigateway';
import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';

export class AppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const stackProps = this.node.tryGetContext('stackProps');

    // Import existing hosted zone (registered domain)
    const hostedZone = route53.HostedZone.fromLookup(this, 'hosted-zone', { domainName: stackProps.domainName });

    // Import domainName we created in 'InfrastuctureStack'
    const domainName = apigw.DomainName.fromDomainNameAttributes(this, 'domain-name', {
      domainName: stackProps.domainName,
      domainNameAliasHostedZoneId: hostedZone.hostedZoneId,
      domainNameAliasTarget: cdk.Fn.importValue('domainNameAliasDomainName'),
    });

    const orderRestApi = new apigw.RestApi(this, 'rest-api-order', {
      deployOptions: {
        stageName: 'prod',
      },
      deploy: true,
      defaultCorsPreflightOptions: {
        allowMethods: ['GET', 'OPTIONS'],
        allowOrigins: apigw.Cors.ALL_ORIGINS,
      },
    });

    // add '/order' base path mapping
    new apigw.BasePathMapping(this, 'base-path-mapping-order', {
      basePath: 'order',
      domainName: domainName,
      restApi: orderRestApi,
    });

    const orderApiLambda = new lambdaNodejs.NodejsFunction(this, 'awesome-api-lambda', {
      entry: './src/order-api-lambda.ts',
      handler: 'handler',
      runtime: lambda.Runtime.NODEJS_18_X,
      environment: {
        NODE_OPTIONS: '--enable-source-maps',
      },
    });

    // /order
    orderRestApi.root.addMethod('GET', new apigw.LambdaIntegration(orderApiLambda));
  }
}

To verify we successfully created a base path mapping with path /order;

1
2
3
4
5
6
7
8
9
10
> aws apigatewayv2 get-api-mappings --domain-name awesome-cdk-examples.com --region us-east-1

{
  "Items": [{
    "ApiId": "5xdi73zy8k",
    "ApiMappingId": "rjem9p",
    "ApiMappingKey": "order",
    "Stage": "prod"
  }]
}

Finally, we should get a success response from our api Lambda fronted by our custom domain.

1
2
3
4
5
6
7
8
9
10
11
> curl -i -X GET 'https://awesome-cdk-examples.com/order'

HTTP/2 200
date: Wed, 22 Mar 2023 12:40:17 GMT
content-type: application/json
content-length: 28
x-amzn-requestid: 9e3c1152-8dc6-4f12-9e25-d33614bcb848
x-amz-apigw-id: CLuPqGBHoAMFd4g=
x-amzn-trace-id: Root=1-641af730-7ce2f0732882625932990183;Sampled=0

{"id":123,"category":"book"}

Same approach can be applied to subdomains or even RestApi constructs to be imported from different stacks.


Synth Stack(s)

1
2
3
> cdk synth

Supply a stack id (InfrastuctureStack, AppStack) to display its template


Deploy Stack(s)

1
> cdk deploy --all


Destroy Stack(s)

Don’t forget to delete the stack after your testing.

1
> cdk destroy --all

Here you can find the related github repository.

This post is licensed under CC BY 4.0 by the author.

API Gateway Lambda Custom Authorizer with AWS CDK

API Gateway Websocket API Example with AWS CDK

Comments powered by Disqus.