Creating AWS Lambda Function URLs with LocalStack
LocalStack now supports Lambda Function URLs! In this article, you will learn how to configure & test a simple AWS Lambda Function URL with Terraform
AWS Lambda is a compute service that lets you run code without provisioning or managing servers. AWS recently launched support for Function URL that allows you to invoke your Lambda function using a URL. It is a simplistic solution to create HTTP(s) endpoints for your Lambda functions and invoke them using requests.
Before introducing the Lambda Function URL, we would have to invoke the Lambda functions through the API Gateway. Now you can create a Lambda function and create a Function URL with little to no customizations. When the Lambda Function URL is invoked, the $LATEST
version of the code is automatically retrieved and executed. You can further test it using cURL
, Postman, or any other API testing tool.
LocalStack supports Lambda Function URL, and you can use it to test your Lambda functions locally via HTTP(s). You can go from creating a Lambda function locally to invoking it using a URL in just a few steps for testing and integration. We will be using creating a Lambda Function URL to scrap the GitHub Trending page and return the top repositories in a JSON response.
Prerequisites
For this tutorial, you will need the following:
- LocalStack to create a Lambda Function URL
awslocal
command-line interface to run local AWS commands against LocalStack- Terraform &
tflocal
to automatically create a local infrastructure against LocalStack jq
&cURL
If you have not installed LocalStack before, you can do it via pip
:
pip install localstack
You can then start LocalStack inside a Docker container by running:
__ _______ __ __
/ / ____ _________ _/ / ___// /_____ ______/ /__
/ / / __ \/ ___/ __ `/ /\__ \/ __/ __ `/ ___/ //_/
/ /___/ /_/ / /__/ /_/ / /___/ / /_/ /_/ / /__/ ,<
/_____/\____/\___/\__,_/_//____/\__/\__,_/\___/_/|_|
💻 LocalStack CLI 1.2.0
[12:03:04] starting LocalStack in Docker mode 🐳 localstack.py:140
preparing environment bootstrap.py:667
configuring container bootstrap.py:675
starting container bootstrap.py:681
[12:03:06] detaching
LocalStack will now run on localhost:4566
, and you can access multiple AWS services locally, including AWS Lambda.
Getting started with Lambda
AWS Lambda is a Serverless Function as a Service (FaaS) system that allows you to write code in your favourite programming language and run it on the AWS ecosystem. Unlike deploying your code on a server, you can now break down your application into many independent functions and deploy them as singular units.
Using LocalStack, as a cloud service emulator, you can deploy your Lambda functions over a mock AWS environment. You can tightly integrate it with other AWS services, like S3, SQS, and more, while running everything inside an ephemeral environment which can be tested and debugged rapidly.
A standard Python Lambda function looks similar to this:
import json
def lambda_handler(event, context):
return {
'statusCode': 200,
'body': json.dumps('Hello from Lambda!')
}
We have a lambda_handler
function executed by the Lambda service every time a trigger event occurs. We would specify this function as an entrypoint for the Lambda function inside a runtime environment (python3.9
in our case). We have also specified event
and context
, the two arguments containing detailed information about the event and the properties that provide information about the invocation.
Let us create a Python script to scrap GitHub Trending repositories. We will use requests
and bs4
as external libraries for our purpose. Create a file named lambda_function.py
and add the following code there:
import json
import requests
from bs4 import BeautifulSoup as bs
def trending():
url = "https://github.com/trending"
page = requests.get(url)
soup = bs(page.text, 'html.parser')
data = {}
repo_list = soup.find_all('article', attrs={'class':'Box-row'})
for repo in repo_list:
full_repo_name = repo.find('h1').find('a').text.strip().split('/')
developer_name = full_repo_name[0].strip()
repo_name = full_repo_name[1].strip()
data[developer_name] = repo_name
return data
def lambda_handler(event, context):
data = trending()
return {
'statusCode': 200,
'body': json.dumps(data)
}
In the above function, we use BeautifulSoup
to scrap the Trending page and store all the repositories inside a dictionary before returning the data to be sent as a JSON response. We will also add a requirements.txt
file to allow us to add dependencies to our deployment package:
requests
bs4
Let us create a deployment package for our Lambda function:
$ pip install -r requirements.txt -t .
$ zip -r function.zip .
It will install all the dependencies inside our root directory and create a deployment package named function.zip
. Let us go ahead and create a Lambda function now:
$ awslocal lambda create-function \
--function-name trending \
--runtime python3.9 \
--timeout 10 \
--zip-file fileb://function.zip \
--handler lambda_function.lambda_handler \
--role cool-stacklifter
{
"FunctionName": "trending",
"FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:trending",
"Runtime": "python3.9",
"Role": "cool-stacklifter",
"Handler": "lambda_function.lambda_handler",
"CodeSize": 1545070,
"Description": "",
"Timeout": 10,
"LastModified": "2022-10-17T17:15:32.573+0000",
"CodeSha256": "5Bck4osMFtRqssI1TSzHyrG2aM46BBpcXdZDF0+4s70=",
"Version": "$LATEST",
"VpcConfig": {},
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "ace2eb2a-767f-4be6-9299-d31aa5de0eae",
"State": "Active",
"LastUpdateStatus": "Successful",
"PackageType": "Zip",
"Architectures": [
"x86_64"
]
}
In the above command, we have outlined the function name trending
and the runtime environment, zip file for our deployment package, handler, timeout seconds, and role. The Lambda function has been created, and you can comfortably invoke it using the awslocal
CLI:
$ awslocal lambda invoke --function-name trending output.txt
In output.txt
, you will find a JSON response containing all the trending repositories on GitHub right now.
Creating a Lambda Function URL
Let us go ahead and create a Lambda Function URL for the above Lambda. To create a Lambda Function URL, all you need to do is to use the create-function-url-config
command and specify the Lambda Function name that we created earlier:
$ awslocal lambda create-function-url-config \
--function-name trending \
--auth-type NONE
{
"FunctionUrl": "http://607a5a09d5d45a8febd22d62c9bb672e.lambda-url.us-east-1.localhost.localstack.cloud:4566/",
"FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:trending",
"AuthType": "NONE",
"CreationTime": "2022-10-17T17:23:07.224906Z"
}
The Lambda Function URL that is created would be visible in the following format:
https://<url-id>.lambda-url.<region>.on.aws
You can now test the above URL using cURL
or Postman. Let us call our Lambda Function URL using cURL
:
$ curl -X GET http://607a5a09d5d45a8febd22d62c9bb672e.lambda-url.us-east-1.localhost.localstack.cloud:4566/ | jq
{
"dragonflydb": "dragonfly",
"dunglas": "frankenphp",
"cisagov": "RedEye",
....
}
Note that the above domain name is unique to the Lambda function and ephemeral in nature. If you spin down LocalStack, the above URL would no longer be accessible. By invoking the URL, you invoke the Lambda function handler and get a JSON output alongside a status code.
Deploying a Lambda Function URL via Terraform
You can automate the above process by orchestrating your AWS infrastructure using Terraform. Terraform allows you to automate the management of AWS resources such as Containers, Lambda functions and so on by declaring them in the HashiCorp Configuration Language (HCL). Terraform ships with terraform
CLI, which allows you to initialize the Terraform backend and apply the resources in a declarative manner.
To create a local infrastructure against LocalStack, we will use the tflocal
CLI. tflocal
uses the Terraform Override mechanism to create a temporary localstack_providers_override.tf
file which is deleted after the infrastructure is created. You can install it via pip
:
pip install terraform-local
Let us create a new file named main.tf
, and use the aws_lambda_function
resource type to create a new Lambda function:
resource "aws_lambda_function" "trending" {
filename = "function.zip"
function_name = "trending"
role = "cool-stacklifter"
handler = "lambda_function.lambda_handler"
source_code_hash = filebase64sha256("function.zip")
runtime = "python3.9"
}
We can now use the aws_lambda_function_url
resource type to create a Lambda Function URL:
resource "aws_lambda_function_url" "lambda_function_url" {
function_name = aws_lambda_function.trending.arn
authorization_type = "NONE"
}
output "function_url" {
description = "Function URL."
value = aws_lambda_function_url.lambda_function_url.function_url
}
The final configuration in our main.tf
file would look like this:
resource "aws_lambda_function" "trending" {
filename = "function.zip"
function_name = "trending"
role = "cool-stacklifter"
handler = "lambda_function.lambda_handler"
source_code_hash = filebase64sha256("function.zip")
runtime = "python3.9"
}
resource "aws_lambda_function_url" "lambda_function_url" {
function_name = aws_lambda_function.trending.arn
authorization_type = "NONE"
}
output "function_url" {
description = "Function URL."
value = aws_lambda_function_url.lambda_function_url.function_url
}
Let us go ahead and initialize our Terraform configuration and apply them:
$ tflocal init
...
Terraform has been successfully initialized!
...
$ tflocal apply --auto-approve
...
aws_lambda_function.trending: Creating...
aws_lambda_function.trending: Creation complete after 7s [id=trending]
aws_lambda_function_url.lambda_function_url: Creating...
aws_lambda_function_url.lambda_function_url: Creation complete after 0s [id=arn:aws:lambda:us-east-1:000000000000:function:trending]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
function_url = "http://d8d26ebfb3fedc0cbb0e4c97bd0612ab.lambda-url.us-east-1.localhost.localstack.cloud:4566/"
...
Since we are using LocalStack, Terraform will create no actual AWS resources. Instead, LocalStack will create ephemeral development resources, which will automatically be cleaned once you stop LocalStack (using localstack stop
). You can now use cURL
with the function_url
again to test the Lambda Function invocation.
Conclusion
While Lambda Function URLs solve well for simplistic use-cases, like creating webhooks or form validators, as it sends the HTTP(s) requests to a single Lambda function. For a more complex use case, API Gateway serves better since it routes requests to various Lambda functions and requires advanced authorization. Using LocalStack, you can develop, debug, and test your Lambda functions in conjunction with a wide range of AWS services. Check out our other blogs on how to hot-reload your Lambda functions using LocalStack and debug it straight from your IDE!
Check out the above code and our extended Lambda Function URL documentation. Thanks to Hashicorp's What is Infrastructure as Code with Terraform?
tutorial for the picture used to describe how Terraform works.