exanubes
Q&A

Api Gateway Websockets #2 Use your custom domain with a Websocket API in AWS Api Gateway

This article is part of a series

  1. Websocket API on AWS Api Gateway with Pulumi
  2. Use your custom domain with a Websocket API in AWS Api Gateway

When creating a new WebSocket API in AWS Api Gateway, AWS will automatically generate a URL for you to connect to. The format is not very user-friendly though, it’s not something you can easily remember and will change if you need to re-create the Api Gateway for some reason. In this article, we’ll see how to use a custom domain name with a WebSocket API in AWS Api Gateway.

TOC

Prerequisites

I’m using Pulumi for writing the infrastructure code. The concepts in this article are the same if you’re using CloudFormation, Terraform or clicking through the AWS Console.

Assuming you have a WebSocket API already created in AWS Api Gateway. If you don’t have one, you can follow the steps in the previous article or use the command below to clone the repository . It goes without saying that you also need to have a domain name that you own.

git clone git@github.com:exanubes/api-gateway-websocket-api.git

Custom domain

By default, the WebSocket Api is available at a generated url that looks something like https://<api-id>.execute-api.<region>.amazonaws.com/<stage>. If you want to use a custom domain you can do so by creating a new domain name inside API Gateway – this has to be a domain that you already own either through Route53 or another domain registrar.

const domainName = new aws.apigatewayv2.DomainName('domain-name', {
	domainName: 'exanub.es',
	domainNameConfiguration: {
		certificateArn: CERTIFICATE_ARN,
		endpointType: 'REGIONAL',
		securityPolicy: 'TLS_1_2'
	}
});
const domainName = new aws_native.apigatewayv2.DomainName('domain-name', {
	domainName: 'exanub.es',
	domainNameConfigurations: [
		{
			certificateArn: CERTIFICATE_ARN,
			endpointType: 'REGIONAL'
		}
	]
});

Then, in the hosted zone of your domain, you need to create an ALIAS record to that api gateway domain name.

new aws.route53.Record('alias', {
	name: 'exanub.es',
	type: aws.route53.RecordType.A,
	zoneId: HOSTED_ZONE_ID,
	aliases: [
		{
			name: domainName.domainNameConfiguration.targetDomainName,
			zoneId: domainName.domainNameConfiguration.hostedZoneId,
			evaluateTargetHealth: true
		}
	]
});
new aws.route53.Record('alias', {
	name: 'exanub.es',
	type: aws.route53.RecordType.A,
	zoneId: HOSTED_ZONE_ID,
	aliases: [
		{
			name: domainName.regionalDomainName,
			zoneId: domainName.regionalHostedZoneId,
			evaluateTargetHealth: true
		}
	]
});
At the time of writing, the @pulumi/aws-native package does not provide a construct for creating a DNS Record

Here, I’m directing traffic to the domain name that I’ve just created within API Gateway using its regional domain name and hosted zone. The regional domain name is the endpoint that AWS API Gateway provides for a custom domain name in a specific AWS region. This domain name allows clients to directly access the API hosted in the specified AWS region. You might have to wait a few minutes for the ALIAS record to work after initial deployment as changes to DNS settings are not usually immediate.

If you're not using Route53, you can look into creating your own Dynamic Provider for creating the record in the registrar you're using. Otherwise, you'll need to setup the ALIAS record manually.

API Mapping Key

I don’t want the domain apex to be used for establishing the WebSocket connection, so I will create an API Mapping and make the websocket endpoint to be exanub.es/chat.

const apiMapping = new aws.apigatewayv2.ApiMapping('api-mapping', {
	apiId: api.id,
	domainName: domainName.domainName,
	stage: stage.name,
	apiMappingKey: 'chat'
});
const apiMapping = new aws_native.apigatewayv2.ApiMapping('api-mapping', {
	apiId: api.id,
	domainName: domainName.domainName,
	stage: stage.name,
	apiMappingKey: 'chat'
});

Worth noting that even though the mapping key is set to chat for the custom domain. It’s still bound to a stage, so when connecting to example.com/chat it will use the dev stage. You will need a different mapping for other stages.

Stage Variables

Now, instead of hardcoding the domain name and api mapping key, we can use stage variables which are actually more convenient than environment variables in this case considering we only have to assign the variables once per stage rather than once per function and it’s also easier to reuse the functions across different stages without risking that test data is saved in a production table for example.

const stage = api.addStage("dev", {
    stageVariables: {
		CONNECTIONS_TABLE: table.arn,
		API_KEY_MAPPING: 'chat'
	}
});

Going back to when we created a stage, let’s add stageVariables prop and pass both the CONNECTIONS_TABLE and API_KEY_MAPPING. With this out of the way, let’s go to the message lambda and replace the invoke api url and table arn with the stage variables.

const handler = async function handler(event) {
  const table = event.stageVariables?.CONNECTIONS_TABLE;
  const apiKeyMapping = event.stageVariables?.API_KEY_MAPPING;
  const body = event.body ? JSON.parse(event.body) : null;
  if (!body) throw new Error("No body");
  if (!table) throw new Error("table cannot be undefined");
  if (!apiKeyMapping) throw new Error("apiMappingKey cannot be undefined");

  const endpoint = `https://${event.requestContext.domainName}/${apiKeyMapping}`;
	//....
} satisfies APIGatewayProxyWebsocketHandlerV2;

The base url in event.requestContext.domainName is now set to the domain you assigned to your API, and instead of using the API stage name we’re using the API Mapping Key that we’ve created earlier. There’s one problem though, the auto-generated invoke url is still active, anyone can use that instead of our own domain name. This means event.requestContext.domainName can be the auto-generated url instead of the custom domain leading to errors.

Disable generated invoke url

To prevent that, we can add disableExecuteApiEndpoint to the Api Gateway resource and set it to true. This will disable the auto-generated invoke url.

 const api = new WebsocketApi("websocket-api", {
    name: "exanubes-websocket-api",
    routeSelectionExpression: "$request.body.type",
    disableExecuteApiEndpoint: true,
  });

Conclusion

Integrating a custom domain name with a WebSocket API in AWS API Gateway significantly enhances the user experience by providing a more memorable and stable URL. By using infrastructure as code tools like Pulumi, you can automate this process, ensuring a seamless setup and management of custom domains. Key steps include creating a custom domain, setting up an ALIAS record in Route53, and using API mappings and stage variables for flexible endpoint management. Disabling the default execute API endpoint further secures your API by enforcing the use of your custom domain. This approach not only improves usability but also aligns with best practices for managing API endpoints in a production environment.

FAQ