Calling an AWS Lambda function URL with IAM authentication in Python
Using Mangum, you can host FastAPI and other ASGI apps on AWS Lambda. If you add a Lambda function URL, you don't even need to set up an API Gateway to expose the HTTP API to other services or the outside world. Even better, the function URL allows you to use AWS IAM to control who's allowed to access the API.
But it turns out that calling a function URL with IAM authentication enabled is not quite that simple. There are two things you have to get right: The configuration of the Lambda function and the HTTP request.
In principle, the AWS documentation contains all the required information and code examples. But it's spread across documentation for different services, lengthy, and full of jargon.
When I set up my first function URL, I couldn't find a complete, end-to-end example. This article fills this gap.
Prerequisites
We'll assume that you have
a Lambda function
my-function-with-url
that exposes an HTTP API anda Lambda function
my-calling-function
that calls this API through an HTTP request.
(You can replace my-calling-function
with code running on any other AWS service.)
Create a function URL configuration
First, we'll create a function URL with IAM authentication for my-function-with-url
:
aws lambda create-function-url-config \
--function-name my-function-with-url \
--auth-type AWS_IAM
You'll find the URL in the response's FunctionUrl
field. If you need it later, you can always retrieve it using the following CLI call:
aws lambda get-function-url-config \
--function-name my-function-with-url \
--query FunctionUrl --output text
Add a resource-based policy
By default, a Lambda function URL with IAM authentication cannot be called by anyone.
According to the documentation, as long as both Lambda functions are within the same AWS account, you can either add a resource-based policy to my-function-with-url
or an identity-based policy to my-calling-function
.
We'll go with the first option and add a resource-based policy to my-function-with-url
that grants my-calling-function
's role the lambda:InvokeFunctionUrl
permission.
Using the AWS CLI, we fetch the ARN of our calling function's execution role and add a policy to my-function-with-url
:
CALLING_FUNCTION_ROLE=$(aws lambda get-function \
--function-name my-calling-function \
--query Configuration.Role --output text)
aws lambda add-permission \
--function-name my-function-with-url \
--action lambda:InvokeFunctionUrl \
--principal $CALLING_FUNCTION_ROLE \
--function-url-auth-type AWS_IAM \
--statement-id allow-my-calling-function-to-invoke-url
Note that it is not possible to simply choose "lambda.amazonaws.com"
as the principal here. Also, keep in mind that cross-account calls to Lambda function URLs require a lambda:InvokeFunctionUrl
permission on the calling side as well. For more information, see the AWS documentation.
Calling from another Lambda
With the function URL configured, it's time to call it.
There are at least two packages available on PyPI that help with creating properly signed and formatted requests: requests-aws4auth
and requests-auth-aws-sigv4
. (It's also possible to assemble the headers and payload yourself.)
However, if you just want to make requests from another service on AWS (like our my-calling-function
), the easiest way is to use botocore
. This library powers both the AWS CLI and boto3
behind the scenes.
The following examples use the requests
library, but you can also use httpx
or any other HTTP client.
GET requests
Here's a minimal example of an HTTP GET
request to a Lambda function URL:
import requests
import botocore.session
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
session = botocore.session.Session()
sigv4 = SigV4Auth(session.get_credentials(),
service_name="lambda",
region_name="eu-central-1")
url = "https://XYZ.lambda-url.eu-central-1.on.aws/book?page=5"
request = AWSRequest(method="GET", url=url)
sigv4.add_auth(request)
signed = request.prepare()
response = requests.get(signed.url, headers=signed.headers)
To adapt this example, you need to replace "eu-central-1"
with your function's AWS region and replace url
with your function's URL.
Both service_name
and region_name
refer to the receiver of the request, not its sender. So even if the code making the request runs in a different AWS service or even outside of AWS, service_name
always has to be set to "lambda"
.
Note that signed.url
and url
do not necessarily differ. But it's a good practice to always use signed.url
in the request. When calling request.prepare()
, the URL might get reformatted, the query parameters get sorted, and new query parameters will be added if you provide them when instantiating AWSRequest
. The URL, including the query string, is part of the signature. Thus, using the original url
will result in authentication failures if any modifications have been made internally.
(Have a look at the source code of SigV4Auth
for all the details.)
POST requests
Here's a minimal example of an HTTP POST
request to a Lambda function URL:
import json
import requests
import botocore.session
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
session = botocore.session.Session()
sigv4 = SigV4Auth(session.get_credentials(),
service_name="lambda",
region_name="eu-central-1")
url = "https://XYZ.lambda-url.eu-central-1.on.aws/login"
data = {"user": "my-user", "token": "ABC123"}
request = AWSRequest(method="POST", url=url, data=json.dumps(data))
sigv4.add_auth(request)
signed = request.prepare()
response = requests.post(signed.url,
headers=signed.headers,
data=signed.body)
To adapt this example, you again need to replace "eu-central-1"
with your function's AWS region, replace url
with your function URL, and data
with your payload.
As with the GET
request, you should always use signed.url
instead of url
to avoid authentication failures due to modifications to the URL while calling request.prepare()
.
It's crucial to pass the payload to AWSRequest
with json.dumps(data)
. Otherwise, request.prepare()
will convert your payload into a query-string-like format.
(Again, I recommend to have a look at the source code of SigV4Auth
for all the details.)
Subscribe to my newsletter
Read articles from Kilian Kluge directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Kilian Kluge
Kilian Kluge
My journey into software and infrastructure engineering started in a physics research lab, where I discovered the merits of loose coupling and adherence to standards the hard way. I like automated testing, concise documentation, and hunting complex bugs.