Lambda Function URLs: An Overview
What are they good for? How much will it cost? And are there alternatives?
Introduction to Lambda Function URLs
Lambda Function URLs landed in April 2022. In many ways they provided a simpler solution to an already solved problem. Everything that can be done with Lambda Function URLs, even to this day, can be achieved through using a public API Gateway.
So what’s the big deal with them?
Well, having an API Gateway is an overhead. Especially if it’s just for a couple of targets sitting behind it. Every request that comes through the API Gateway is billable. And it’s not cheap either.
The current going rate as of time of writing is $3.50/million requests. Yes, there’s no upfront costs, but this is one of the few AWS services based around HTTP that make a flat charge. Most services take into account the duration and computation involved. But in this case, a 1KB payload is no different to a 1GB payload entering the system.
To put this in perspective, the cost per request (“invocation”) for a standard Lambda Function in London (EU-WEST-2) is $0.20/million requests. That’s nearly a 18 fold difference for the same request.
A popular site in the UK, Cinch, are reported to have up to 4.5 million requests per day- that’s nearly 135 million requests a month1, which works out at a $472/month overhead.
Assuming every hour 1GB gets consumed (much higher than your average typical request), using an Application Load Balancer would cost under the $50 mark comfortably.
Of course, we can’t assume to be able to swap out services on a 1-1 basis given the use case, but it really does put an expensive sticker tag on the API Gateway if you’re not fully utilising the capabilities it offers.
Use Cases
So what are the tradeoffs then? It seems like an 18 fold difference would always come with some, otherwise everyone would be using this new tech as a no-brainer.
Lambda Function URLs can’t (in comparison to API Gateway):
Rate limit users based on identity
Sit behind a WAF (Web Application Firewall) directly
No custom domain - once deployed, you’re stuck with a randomly generated URL forever
Have advanced monitoring (request latency, data transfer metrics)
Restricted to one endpoint per lambda function
These limitations mean that there are specific use cases, for instance:
Webhooks connecting to your inner AWS services
Internal tooling to trigger a CRUD action
Prototyping
Cost Comparison
To do any kind of black and white comparison like this, we’ll have to make a few assumptions:
5 million requests
128MB assigned memory in US East (N. Virginia)
An average invocation times of 50ms
Ignoring data egress costs
API Gateway + Lambda
Lambda: $1.53/month
API Gateway: $17.50/month ($3.50/million request)
Total Costs: $19.03/month
Lambda Function URLs
Lambda: $1.53/month
Total Costs: $1.53/month
That represents 12x cost difference between the two architectures.
Of course, the more requests you have going through, the cheaper it gets both for the API Gateway + Lambda. But for the majority of use cases where Lambda Function URLs are applicable for, it seems unreasonably expensive.
URL Anatomy and The Public Internet
When a Lambda Function URL gets deployed, you get a URL that is associated with that Lambda function.
It looks like this:
https://<RANDOM-ID>.lambda-url.<REGION>.on.aws
The random ID (<RANDOM-ID>
) is determined on the initial deployment, and the region (<REGION>)
is the region in which the function is deployed in.
All deployed URLs will be using the https
protocol. This is something that’s not configurable.
Using Serverless Framework to handle our infrastructure as code
// serverless.yml
service: lambda-urls-demo
provider:
name: aws
stage: dev
region: eu-west-2
functions:
test:
handler: handler.demoHandler
url: true
/*
handler.js
*/
function demoHandler(event) {
return {
statusCode: 200
}
}
module.exports = {
demoHandler
}
Running: serverless deploy --aws-profile PROFILE
Deploying lambda-urls-demo to stage dev (eu-west-2)
✔ Service deployed to stack lambda-urls-demo-dev (34s)
endpoint: https://v2olvkdtht02sy5utbvzshbcei0szimt.lambda-url.eu-west-2.on.aws/
functions:
test: lambda-urls-demo-dev-test (1.9 kB)
Observe the endpoint exposed, notice how it’s set to HTTPS. By design these accessible on the public internet.
There isn’t any way to associate the Lambda Function URL to a private VPC. If this is a deal breaker, then naturally Lambda Function URLs aren’t the correct choice for your architecture.
Securing Lambda Function URLs
Lambda Function URLs allow you to set the authentication type. This can either be configured as AWS_IAM
or NONE
.
AWS_IAM
is a good default choice. This allows delegating authorisation to the AWS IAM service.
However there are “gotchas” with this, the main one being how one actually calls the Lambda Function URL when this is set.
// serverless.yml
service: lambda-urls-demo
provider:
name: aws
stage: dev
region: eu-west-2
functions:
test:
handler: handler.demoHandler
url:
authorizer: aws_iam # Enables AWS_IAM authorization
With AWS_IAM
enabled, this requires the request made to be pre-signed before sending out the request.
The simplest way to verify that it works without worrying about how to implement signature generations is via docker using a tool called awscurl.
docker run --rm -it okigan/awscurl
--access_key ACCESS_KEY
--secret_key SECRET_KEY
--region REGION
--service lambda
www.12345.lambda-url.eu-west-2.on.aws
What actually happens when you call this is the following:
Request payload gets pre-signed with your AWS credentials
That pre-signed signature gets put into the header of the request
AWS receives the request, then calculates the expected signature of their end
AWS matches their expected signature, with the one provided in the header (from step 2)
If the signatures don’t match, the request gets rejected
'Fire-walling' your Lambda Function URL
Lambda Function URLs contain the user’s IP address in the event payload. We can use this to further enhance the security of our lambda function.
/*
handler.js
*/
function demoHandler(event) {
const clientIpAddress = event["headers"]["x-forwarded-for"];
const whitelistedIps = [
"133.33.33.7"
];
if (!whitelistedIps.includes(clientIpAddress)) {
return {
statusCode: 401
}
}
return {
statusCode: 200
}
}
module.exports = {
demoHandler
}
We can observe the event’s x-forwarded-ip
header which will reveal the requestor’s public IP address. If our the whitelisted IP doesn’t match, we can reject the request within the application code. This may be more suitable if the consumer doesn’t have an IAM
user.
We could take this a step further by involving a stateful database to manage these IPs such as DynamoDB. The tradeoff being you have the overhead of managing whitelisted IP addresses.
Retrieving the AWS_IAM user identity
We can also retrieve within the event payload who made the request, if we opt in for the AWS_IAM
authentication mode.
This may be useful for auditing purposes or for other reasons downstream in the application that requires the identity of the caller.
function demoHandler(event) {
const userArn = event["requestContext"]["authorizer"]["iam"]["userArn"];
console.log(userArn); // arn:aws:iam::XXXXXXXXXXXX:USER
console.log(JSON.stringify(event["requestContext"]));
/*
"accountId": "390901917839",
"apiId": "v2olvkdtht72sy4utbvvshbcwi0szimt",
"authorizer": {
"iam": {
"accessKey": "XYZ",
"accountId": "XXXXXXXXXXXX",
"callerId": "XXXXXXXXXXXX",
"cognitoIdentity": null,
"principalOrgId": "o-1YYYYYYYY",
"userArn": "arn:aws:iam::XXXXXXXXXXXX:USER",
"userId": "XXXXXXXXXXXX"
}
},
"domainName": "v2olvkdtht02sy5utbvzshbcei0szimt.lambda-url.eu-west-2.on.aws",
"domainPrefix": "v2olvkdtht02sy5utbvzshbcei0szimt",
"http": {
"method": "GET",
"path": "/",
"protocol": "HTTP/1.1",
"sourceIp": "185.241.227.234",
"userAgent": "python-requests/2.30.0"
},
"requestId": "89381aa6-0d46-4cd5-8b2e-8f7c7f6c223b",
"routeKey": "$default",
"stage": "$default",
"time": "23/May/2023:20:29:39 +0000",
"timeEpoch": 1684873779193
*/
return {
statusCode: 200
}
}
module.exports = {
demoHandler
}
Conclusion
Lambda Function URLs offers an opportunity of major cost savings and the ability to get started as soon as possible.
It's never been faster to deploy out a piece of business logic to the public internet before, but with this comes with the right use cases.
Webhooks are without a doubt the best candidate for this, they are typically event driven and applications typically authenticate through access tokens or API Keys.
API Gateway can also offer this natively, but at the overhead cost of the managing the API Gateway is overkill.
Single function microservices that permit transiting data over the public internet may also be the right choice here.
However, any microservice that has multiple functions involved would result in multiple URLs to manage. This may add extra overhead in the long run, so is not advisable. The API Gateway abstracts this through stages under the same rest API ID, i.e www.<rest-id>.apigateway.aws.com/<stage>/url-1
.
Finally, sensitive data that should flow within a private VPC would not be suitable here. A private API gateway would be more appropriate through configuring the lambda function to be attached to the private VPC.
https://aws.amazon.com/blogs/startups/scale-your-startup-with-serverless-on-aws