We have a lambda running but it’s only accessible via the aws cli lambda invoke
command.
To be able to access it on a URL, we need to use another AWS service: API Gateway.
API Gateway is a service that lets us define HTTP, Rest, and Websocket API URLs, and then separately define where to send the traffic when it arrives. It can also can handle authentication, CORS, and other tasks for us.
Think of API Gateway as the place we define how people access the resources (such as lambdas) that we’ve deployed.
We’ll be using the http
integration for the apigatewayv2
set of APIs, so we need to install a couple packages into our CDK application.
npm i @aws-cdk/aws-apigatewayv2-integrations-alpha @aws-cdk/aws-apigatewayv2-alpha
We’ll add requires for the two new packages in lib/infra-stack.js
.
const integrations = require("@aws-cdk/aws-apigatewayv2-integrations-alpha");
const apigateway = require("@aws-cdk/aws-apigatewayv2-alpha");
Then below where we define our pokemonLambda
, add a new HttpLambdaIntegration
and a new HttpApi
.
const pokemonLambda = ...
const pokemonIntegration = new integrations.HttpLambdaIntegration(
"PokemonIntegration",
pokemonLambda
);
const httpApi = new apigateway.HttpApi(this, "pokemon-api");
httpApi.addRoutes({
path: "/pokemon/{pokemon}",
methods: [apigateway.HttpMethod.GET],
integration: pokemonIntegration,
});
AWS Gateway uses the term HttpApi
to refer to the URL routes that are available for consumers to access, and integration
to refer to the resources that will handle the http request.
The HttpLambdaIntegration
is given a name and the lambda we previously constructed.
const pokemonIntegration = new integrations.HttpLambdaIntegration(
"PokemonIntegration",
pokemonLambda
);
Then we create the httpApi
, which accepts the current context and a name as well.
const httpApi = new apigateway.HttpApi(this, "pokemon-api");
We can then add any number of routes to the httpApi
using addRoutes
. This takes a path, in this case we’ve defined the root of the url as /pokemon
and the second part as a path parameter that represents the name of a Pokemon.
We can restrict it to specific HTTP methods, in this case we choose to allow only GET
methods.
Finally we associate this path with the Pokemon lambda integration.
httpApi.addRoutes({
path: "/pokemon/{pokemon}",
methods: [apigateway.HttpMethod.GET],
integration: pokemonIntegration,
});
Diffing the CDK deployment at this point will show us a number of new resources.
The IAM Statement Changes show us that the Principal
, which is the API Gateway service, now has access to invoke our lambda.
We’ve already talked about the API, Integration, Route, and Permission, which leaves the Stage.
We won’t be covering Stages here but if you’ve ever used “dev”, “staging”, or “production” environments you’ll understand what they are. In this case we’re using a single default stage.
❯ npm run cdk diff -- --profile rust-adventure-playground
> infra@0.1.0 cdk
> cdk "diff" "--profile" "rust-adventure-playground"
Stack InfraStack
IAM Statement Changes
┌───┬────────────┬────────┬────────────┬────────────┬─────────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼────────────┼────────┼────────────┼────────────┼─────────────┤
│ + │ ${PokemonH │ Allow │ lambda:Inv │ Service:ap │ "ArnLike": │
│ │ andler.Arn │ │ okeFunctio │ igateway.a │ { │
│ │ } │ │ n │ mazonaws.c │ "AWS:Sour │
│ │ │ │ │ om │ ceArn": "ar │
│ │ │ │ │ │ n:${AWS::Pa │
│ │ │ │ │ │ rtition}:ex │
│ │ │ │ │ │ ecute-api:$ │
│ │ │ │ │ │ {AWS::Regio │
│ │ │ │ │ │ n}:${AWS::A │
│ │ │ │ │ │ ccountId}:$ │
│ │ │ │ │ │ {pokemonapi │
│ │ │ │ │ │ 956F0EB2}/* │
│ │ │ │ │ │ /*/pokemon/ │
│ │ │ │ │ │ {pokemon}" │
│ │ │ │ │ │ } │
└───┴────────────┴────────┴────────────┴────────────┴─────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)
Resources
[+] AWS::ApiGatewayV2::Api pokemon-api pokemonapi956F0EB2
[+] AWS::ApiGatewayV2::Stage pokemon-api/DefaultStage pokemonapiDefaultStage08CBDEFB
[+] AWS::ApiGatewayV2::Integration pokemon-api/GET--pokemon--{pokemon}/PokemonIntegration pokemonapiGETpokemonpokemonPokemonIntegrationB24850F1
[+] AWS::Lambda::Permission pokemon-api/GET--pokemon--{pokemon}/PokemonIntegration-Permission pokemonapiGETpokemonpokemonPokemonIntegrationPermissionEC10B5A9
[+] AWS::ApiGatewayV2::Route pokemon-api/GET--pokemon--{pokemon} pokemonapiGETpokemonpokemonB169038E
We’ll need the URL of our new endpoint, which we could find in the AWS console. We can also take advantage of CfnOutput
which lets us output values after using cdk deploy
.
Add CfnOutput
to the destructured imports for aws-cdk-lib
.
const { Stack, CfnOutput, aws_dynamodb, aws_lambda } = require("aws-cdk-lib");
And then we can set up a series of outputs for the DynamoDB Table Name, the Lambda Function Name, and the Pokemon API base URL.
new CfnOutput(this, "pokemonTable", {
value: pokemonTable.tableName,
description: "The name of the DynamoDB Table",
});
new CfnOutput(this, "pokemonLambda", {
value: pokemonLambda.functionName,
description: "The name of the Pokemon Lambda",
});
new CfnOutput(this, "pokemonBaseUrl", {
value: httpApi.apiEndpoint,
description: "The root URL for the pokemon endpoint",
});
Now when we deploy, we’ll see a new Outputs
section that has all the values we need.
Outputs:
InfraStack.pokemonTable = InfraStack-PokemonTable7DFA0E9C-1II2IAD7OZ2EJ
InfraStack.pokemonLambda = InfraStack-PokemonHandlerC37D7DE3-Wk07o9gAXaiO
InfraStack.pokemonBaseUrl = https://72i5uisgr5.execute-api.us-east-1.amazonaws.com
We can also get a JSON file output using --outputs-file ./cdk-outputs.json
when deploying, which creates a file that looks like this.
{
"InfraStack": {
"pokemonTable": "InfraStack-PokemonTable7DFA0E9C-1II2IAD7OZ2EJ",
"pokemonBaseUrl": "https://72i5uisgr5.execute-api.us-east-1.amazonaws.com",
"pokemonLambda": "InfraStack-PokemonHandlerC37D7DE3-Wk07o9gAXaiO"
}
}
Take the base URL and add our Pokemon route to the end, then either curl it or visit it in a browser. In this case I’ve chosen to use the Pokemon bulbasaur
.
curl https://72i5uisgr5.execute-api.us-east-1.amazonaws.com/pokemon/bulbasaur
You’ll receive the empty data response from our lambda if everything is working.
{"data":{}}
If we head over to CloudWatch Logs in the AWS Console, go to “Log Groups” and select our function’s logs: we can see a number of requests coming in to our lambda.
START RequestId: 93dcad1d-07f8-4b71-8044-50ff3374f2b2 Version: $LATEST
END RequestId: 93dcad1d-07f8-4b71-8044-50ff3374f2b2
REPORT RequestId: 93dcad1d-07f8-4b71-8044-50ff3374f2b2 Duration: 1.25 ms Billed Duration: 35 ms Memory Size: 1024 MB Max Memory Used: 16 MB Init Duration: 33.46 ms
START RequestId: 39974500-4923-47f4-93cf-dff44de535e5 Version: $LATEST
END RequestId: 39974500-4923-47f4-93cf-dff44de535e5
REPORT RequestId: 39974500-4923-47f4-93cf-dff44de535e5 Duration: 0.80 ms Billed Duration: 1 ms Memory Size: 1024 MB Max Memory Used: 16 MB
START RequestId: 2805016a-4614-4bb4-b2c2-a8211f712af4 Version: $LATEST
END RequestId: 2805016a-4614-4bb4-b2c2-a8211f712af4
REPORT RequestId: 2805016a-4614-4bb4-b2c2-a8211f712af4 Duration: 1.12 ms Billed Duration: 2 ms Memory Size: 1024 MB Max Memory Used: 16 MB
Our lambda is currently cold-starting in 30ms, and running warm for 1ms. This is pretty expected as we aren’t doing much besides responding and Rust tends to be exceedingly efficient anyway.
Our lambda is now accessible on a public URL.