Creating AWS Lambda Function URLs with LocalStack

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

ยท

7 min read

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.

Architecture of Lambda Function URL โ€“ Users will invoke a REST API which will trigger a Lambda function and return the scrapped web data from GitHub as a JSON response

Prerequisites

For this tutorial, you will need the following:

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

Simplistic description of how Terraform works

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.

ย