Apr 06 2022
AWS announced the release of the Lambda Function URLs feature today. In this post, I describe what it is, how it works, and how you can benefit from it.
API Gateway and AWS Lambda is a potent combination and lets you build REST APIs without having to worry about the underlying infrastructure. API Gateway offers many powerful features out-of-the-box, including:
- authentication and authorization
- request throttling
- quotas and usage plans
- integration with AWS WAF
- request and response mapping
- service proxy to other AWS services
- web sockets
Understandably you pay a premium for these features. The cost of API Gateway is often higher than the respective Lambda functions in a production application. The cost is entirely justified if your application makes heavy use of these features.
But what if you don’t need all the bells and whistles? Maybe you just need a public API with CORS support or an internal API that is protected by AWS_IAM? It’s rather inefficient to be paying for the full price of API Gateway in these cases.
With the Lambda Function URLs feature announced today, we can create REST APIs backed by Lambda functions without API Gateway.
When you create a new function, check the Enable function URL
box under Advanced settings
. As you can see from the screenshot, you can optionally enable CORS and use AWS IAM as the authorization method for the API.
The resulting function URL looks like this:
https://{url-id}.lambda-url.{region}.on.aws
where the url-id
is a random ID.
You can also create the function URL using CloudFormation, by adding a AWS::Lambda::Url
resource like this:
MyLambdaFunctionUrl: Type: AWS::Lambda::Url Properties: TargetFunctionArn: !Ref MyLambdaFunction AuthType: NONE Cors: AllowOrigins: - https://lumigo.io
MyLambdaFunctionUrlPublicPermission: Type: AWS::Lambda::Permission Properties: FunctionName: !Ref MyLambdaFunction Principal: '*' Action: 'lambda:InvokeFunctionUrl' FunctionUrlAuthType: NONE
As a launch partner for this exciting new feature, Lumigo is already supporting function URLs. When you view the details of a Lambda function in Lumigo, you can see its function URL (if one is configured).
And now, let’s explore function URLs some more and when you should use them.
Authorization
For internal APIs, you should use AWS_IAM
as the authorization type. The caller has to sign the HTTP request using the AWS v4 signing process, and the caller must also have the lambda:InvokeFunctionUrl
IAM permission against the function.
Once a function URL has been created, you can still change the authorization type. But bear in mind that changes to the authorization type take about a minute to take effect.
For public APIs, you need to set the authorization type to NONE
. Additionally, you need to set the resource-based policy for the function to allow for unauthenticated invocation. To do that, you can call the AddPermission
API (see official documentation here) with the following parameters:
Principal
is*
Action
islambda:InvokeFunctionUrl
Condition
islambda:FunctionUrlAuthType == NONE
Without this resource-based policy, any request to the function URL will be rejected with an HTTP 403 status code even if the AuthType is configured as NONE
.
Request and Response format
A great thing about this new feature is that it uses the same schema format as API Gateway payload format 2.0 (the format used by API Gateway HTTP APIs). So you don’t have to change your code to switch from API Gateway to function URL.
As with API Gateway HTTP APIs, your Lambda function’s return value will be interpreted as an HTTP 200 response with Content-Type
as application/json
:
exports.handler = async (event) => { return { message: 'hello' } }
But if you need to take control of the response and say, add a custom response header, then you can still use the verbose response format.
exports.handler = async (event) => { return { statusCode: 200, body: JSON.stringify({ message: 'hello' }), headers: { 'x-message': 'custom header' } } }
Handling different paths
All requests to subpaths of the function URL would be forwarded to the Lambda function. For example, a GET request to https://{url-id}.lambda-url.{region}.amazonaws.com/users/123
would invoke the Lambda function with a payload like this:
{ "version": "2.0", "routeKey": "$default", "rawPath": "/user/1234", "rawQueryString": "", "cookies": [ "..." ], "headers": { "sec-fetch-mode": "cors", "sec-fetch-site": "none", "accept-language": "...", "cookie": "...", "x-forwarded-proto": "https", "x-forwarded-port": "443", "x-forwarded-for": "...", "accept": "*/*", "sec-ch-ua": "...", "sec-ch-ua-mobile": "?0", "x-amzn-trace-id": "Root=1-619169b2-3475a75b6f6418026f27322e", "sec-ch-ua-platform": "\"macOS\"", "host": "<url-id>.lambda-url.eu-south-1.amazonaws.com", "content-type": "application/json", "cache-control": "no-cache", "accept-encoding": "gzip, deflate, br", "sec-fetch-dest": "empty", "user-agent": "..." }, "queryStringParameters": null, "requestContext": { "accountId": "...", "apiId": "...", "authentication": null, "authorizer": null, "domainName": "url-id.lambda-url.eu-south-1.amazonaws.com", "domainPrefix": "...", "http": { "method": "GET", "path": "/user/1234", "protocol": "HTTP/1.1", "sourceIp": "...", "userAgent": "..." }, "requestId": "caa072ed-ad63-4ee9-8f8a-532eca0d95ee", "routeKey": "$default", "stage": "$default", "time": "14/Nov/2021:19:55:30 +0000", "timeEpoch": 1636919730206 }, "body": null, "pathParameters": null, "stageVariables": null, "isBase64Encoded": false }
Your Lambda function can handle different HTTP verbs and URL paths and do the right thing:
GET /users/{userId}
: returns the user with the idPOST /users
: creates a new user
and so on.
Request throttling
You can use Lambda reserve concurrency to implement basic request throttling. For example, if you were to set a Lambda function’s reserved concurrency to 1, then a second concurrent request would be throttled adn the caller would receive a 4xx HTTP error.
Custom domains
If you want to use a user-friendly domain for your API, then you can create a CloudFront distribution and point it at the function URL as the origin.
You can also stitch multiple function URLs together this way. For example, you may have a few Lambda functions each handling the CRUD operations associated with several entities — users, products and orders. Each function would have its own function URL, and you can present them as subpaths under a root domain:
- https://myapi.com/users/…
- https://myapi.com/orders/…
- https://myapi.com/products/…
To do this, you need to add the function URLs as origins for the CloudFront distribution. And then configure behaviours that match the subpaths to the associated Lambda URL, like this:
When you use CloudFront with function URLs, you can also take advantage of CloudFront’s edge caching. And you can integrate the CloudFront distribution with AWS WAF (Web Application Firewall)and protect your API with a host of built-in WAF rules or create custom rules that are tailored for your API.
When do you need to use API Gateway?
As you can see, you can imitate many of API Gateway’s basic capabilities with function URLs, including custom domains and request throttling.
However, there are still plenty of reasons to upgrade to API Gateway. For example, you can secure APIs with Cognito User Pool or any OpenID Connnect providers. This allows you to delegate user authentication and authorization to API Gateway and the identity provider.
Similarly, SaaS applications often need to track and limit usage by tenant, and API Gateway usage plans is a great way to implement these features and monetize your API. For example, free users are limited to 100 requests per day, bronze-tier users 1000 requests per day, silver-tier users 50,000 requests per day, and so on.
And if you want to build real-time applications, then you might also want to upgrade to API Gateway to use its WebSockets API.
That said, for simple APIs that don’t need these advanced features, now you have to option to save on your AWS bill and use Lambda function URLs instead!