Thanks!
A verification email has been sent to
Previously, we've learned how API Gateway works – focusing on microservice architecture. However, API Gateway also works well with serverless and in this article we'll go over how to build an api gateway with a custom domain and a lambda integration using aws cdk. Finished code is available on github
Aside from aws-cdk I have used an additional tool that combines Serverless Application Model (SAM) with aws-cdk.
You can install it with homebrew:
Double click to copybrew install aws-sam-cli-beta-cdk
In order to deploy this infrastructure to aws, you will also need a hosted zone in AWS Route 53 and a certificate in us-east-1
region.
In order to create an API with a custom domain we're going to need the following:
Double click to copy1// /lib/api-gateway.stack.ts2export class ApiGatewayStack extends cdk.Stack {3 constructor(app: cdk.App) {4 super(app, ApiGatewayStack.name)5 const api = new RestApi(this, "api-gateway", {6 defaultCorsPreflightOptions: {7 allowOrigins: ["http://localhost:3000"],8 },9 })10 }11}
This short snippet of code will create an API Gateway that will have CORS configured to only allow requests from http://localhost:3000
. You can also leave it empty or change it to Cors.ALL_ORIGINS
to explicitly allow all origins to call your api.
In a production scenario maybe you'd like only your private website to be able to call you API. This is where you can set this up.
To set up a lambda we need to first create the lambda function and then create a lambda AWS Resource for our infrastructure.
Double click to copy1// /lambda/hello-world/hello-world.ts2export async function main(3 event: APIGatewayProxyEvent,4 context: Context,5 callback: Callback6) {7 const name = event?.queryStringParameters?.name8 if (!name) {9 return callback(new Error("Name cannot be undefined"))10 }11 return {12 statusCode: 200,13 body: "Hello " + name,14 headers: {15 "Access-Control-Allow-Origin": "http://localhost:3000",16 "Content-Type": "text/plain",17 },18 }19}
All this lambda does is take out a name query string parameter and return a greeting message. Important to note that headers are especially important as we have configured CORS to only allow certain origins.
Without Access-Control-Allow-Origin
the OPTIONS
http request would return an error. So if you're making a request from the correct origin and everything seems to be working fine but you get an error anyway, check headers on your lambda function.
Double click to copy1// /lib/lambda.stack.ts2export class LambdaStack extends cdk.Stack {3 lambdaFunction: IFunction4 constructor(app: cdk.App) {5 super(app, LambdaStack.name)6 this.lambdaFunction = new Function(this, "hello-world", {7 memorySize: 128,8 timeout: cdk.Duration.seconds(5),9 runtime: Runtime.NODEJS_14_X,10 handler: "hello-world.main",11 code: Code.fromAsset(path.join(__dirname, "/../lambda/", "hello-world")),12 })13 }14}
To create a lambda resource we need to pass a handler, which is the file and function names and then load the code from that file by passing a path to it. Last but not least we defined the runtime and timeout. This basically sets a hard stop for the lambda if it doesn't finish its job in 5 seconds in this case. As this is not a very computationally heavy lambda, we can also set a low memorySize of 128Mb. This is also the default setting, however, I prefer to have it explicitly set.
One very cool feature of SAM is the ability to run lambdas locally. In the file structure, there is a events
directory that holds an API Gateway proxy event that can be used when triggering the lambda with
Double click to copysam-beta-cdk local invoke LambdaStack/hello-world -e events/hello-world.json
Keep in mind that LambdaStack
is the name of the stack I used and hello-world
is the id
for the Function
resource inside that stack. These could be different for you.
Double click to copy1// /lib/api-gateway.stack.ts2constructor(app: cdk.App, lambdaFunction: IFunction){3 // ...4 const helloIntegration = new LambdaIntegration(lambdaFunction)5 const hello = api.root.addResource('hello')6 hello.addMethod("GET", helloIntegration)7}
First we need to create a lambda integration which basically tells API Gateway with which service it is going to be integrated as it does not have to be a Lambda. It could be an SQS or even a DynamoDB.
Creating a resource basically means creating a rest endpoint. Let's say this resource was users
instead of hello
and I'd like to have an endpoint to fetch a user by id.
I would then take the users
resource and call addResource
in order to create another part of the REST endpoint. Wrapping the resource string in curly brackets {}
sets it as a dynamic path parameter.
Double click to copy1declare const users: Resource2const user = users.addResource('{userId}')
Last but not least, unless we define an HTTP Method and assign the lambda integration to it, we will not be able to receive data from our resource.
In order to create a domain name, we will need an SSL certificate which can be requested in AWS Certificate Manager if you don't already have one.
If you want to optimise your API for performance, you will use an EDGE endpoint type – meaning you want to use Cloudfront and its Edge Locations – and have to have your certificate in N. Virginia us-east-1
region.
Otherwise, if you don't need that kind of optimisation, you will use a REGIONAL endpoint type and have to have your certificate in the same region as your API Gateway.
Once you have the certificate, paste the arn into the project
Double click to copy1// /lib/api-gateway.stack.ts2const certificateArn =3 "arn:aws:acm:us-east-1:12345678901234:certificate/xxx-yyy-zzzz-aaaa-bbbbbb"
API Gateway provides us with a standardized aws url, however, for the most part we'd probably prefer to use our own domain address. To achieve that we're going to create a domain name, set a mapping to that domain from the API Gateway and then create a CName record to redirect to the cloudfront distribution.
Double click to copy1// /lib/api-gateway.stack.ts2const domain = new DomainName(this, "api-gw-domain-name", {3 domainName: "custom.example.com",4 certificate,5 securityPolicy: SecurityPolicy.TLS_1_2,6 endpointType: EndpointType.EDGE,7})
Most likely, example.com would be the domain for our website, that's why I opted for a custom
subdomain. Depending on the region you requested a certificate in, you can setup either EDGE or REGIONAL endpoint type.
There is also a PRIVATE option which would only accept requests from your VPC.
Double click to copy1// /lib/api-gateway.stack.ts2new BasePathMapping(this, "api-gw-base-path-mapping", {3 domainName: domain,4 restApi: api,5})
BasePathMapping is exactly what it sounds like, it creates a base path that user has to use when calling the api. It's very similar to a mechanism used in frontend applications.
In this case we're only setting a domain name, but by using a basePath
property we could enforce a base path after the domain name as well e.g., custom.example.com/required-base-path
.
This could be an alternative to using a subdomain for API Gateway.
Double click to copy1// /lib/api-gateway.stack.ts2const hostedZone = HostedZone.fromHostedZoneAttributes(this, "hosted-zone", {3 hostedZoneId: "Z00971403R2PNG6AZVK4K",4 zoneName: "exanubes.com",5})67new CnameRecord(this, "api-gw-custom-domain-cname-record", {8 recordName: "custom",9 zone: hostedZone,10 domainName: domain.domainNameAliasDomainName,11})
At last, we have to create a CName record to map the custom
subdomain to the Cloudfront distribution in our Route53 hosted zone. Keep in mind that should you use a different hosting service, like Netlify, you will need to add the record there manually. It will not work otherwise.
Now that everything's ready we can build and deploy the application
Double click to copy1npm run build2npm run cdk:deploy
It should be a secure API that is not be accessible from different origins than defined. When you're finished testing don't forget to tear it all down
Double click to copynpm run cdk:destroy
In this article we've gone over setting up an API Gateway with a lambda integration using aws cdk and how to invoke lambdas locally – without deployment. Then we created a custom domain and mapped it to the API Gateway using BasePathMapping
.
To finish it off and point users to API Gateway when calling the new domain, we had to create a CName record that will resolve to the Cloudfront distribution.
A verification email has been sent to