Serverless applications are becoming popular among the DevOps community because they do not require application server infrastructure. AWS Lambda is an event-driven offering from AWS that can help you run any application without the need for application servers.
In this blog post, we will discuss AWS Lambda and how to use Terraform to manage it. In the tutorial, we will start with the basics of the AWS Lambda function and then deploy a very simple Python app using it.
What we will cover:
AWS Lambda is a serverless compute service that executes code in response to events without the need for provisioning or managing servers. Although you can create and run Lambda functions from the AWS web console, Terraform offers a great solution for managing Lambda functions through Infrastructure as Code (IaC).
In Terraform, an AWS Lambda function is defined using the aws_lambda_function resource. This resource specifies key details such as the function’s name, runtime environment (e.g., Python, Node.js), handler method, and source code location (either inline or from an S3 bucket). Additionally, configurations for memory, timeout, environment variables, and permissions can also be set up within this resource.
For example, deploying a Python script to AWS Lambda with Terraform requires the following steps:
- Install Terraform
- Configure the AWS account
- Set up the IAM roles and policies
- Write the Python application
- Create a ZIP file of the Python application
- Add the aws_lambda_function
- Run and execute Terraform main.tf
- Verify the Lambda function from the AWS console
AWS Lambda function is very flexible and supports various use cases:
- Event-driven applications — AWS Lambda can be triggered from a number of different event sources. Examples include triggering when a database row is updated, and triggering when the message count in a queue is greater than a certain number.
- Web and mobile backends — For a Mobile touchscreen, GUI event can be integrated with AWS Lambda using AWS API Gateway to trigger the Lambda function by calling the microservice-http-endpoint. (Learn how to create API Gateway using Terraform)
- Machine learning and data processing — As AWS Lambda supports Python, it is well suited for machine learning and data analytics applications. ML and Data Analytics applications rely heavily on Python and its libraries (Numpy, TensorFlow, MatPlotlib, Scipy, etc.).
Let’s take a deeper look at Terraform Lambda resources and how they provision Lambda functions and other resources.
- aws_lambda_function — The aws_lambda_function resource creates an AWS lambda function inside your AWS account. You can provide the code via an S3 bucket or local ZIP file and set configuration details such as the handler, the runtime, and other parameters as well.
resource "aws_lambda_function" "this" {
filename = "lambda_function.zip"
function_name = "example_lambda"
role = aws_iam_role.lambda_role.arn
handler = "index.handler"
runtime = "nodejs14.x"
}
- aws_lambda_alias — By using the aws_lambda_alias resource in Terraform, you can set up an alias for your AWS lambda function. With this alias, you can point to a specific version of your lambda function, making it easier to use different versions of your functions.
resource "aws_lambda_alias" "this" {
name = "dev"
function_name = aws_lambda_function.this.function_name
function_version = aws_lambda_function.this.version
}
- aws_lambda_permission — Lambda permissions give other services permissions to invoke your lambda functions.
resource "aws_lambda_permission" "this" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.this.function_name
principal = "apigateway.amazonaws.com"
}
- aws_lambda_layer_version — A layer version resource creates a lambda layer for your functions which is a zip archive containing shared code or libraries shared by your lambda functions.
resource "aws_lambda_layer_version" "this" {
filename = "lambda_layer.zip"
layer_name = "my_nodejs_layer"
compatible_runtimes = ["nodejs14.x"]
}
- aws_api_gateway_rest_api — The aws_api_gateway_rest_api creates an API Gateway in AWS. You can define endpoints that invoke Lambda functions, acting as a front door for your applications.
resource "aws_api_gateway_rest_api" "this" {
name = "my_api_gateway"
description = "API Gateway for my nodejs app"
}
- aws_cloudwatch_log_group — A cloud watch log group can be used to monitor, store, and access log files from various AWS services.
resource "aws_cloudwatch_log_group" "lambda_log_group" {
name = "/aws/lambda/my_lambda_function"
retention_in_days = 14
}
Let’s start with the prerequisites for this managing AWS Lambda function with Terraform tutorial.
- You should have Terraform installed onto your machine (see our tutorial here: How to Download and Install Terraform on Windows, Linux & macOS)
- You must have an AWS account.
Before proceeding further, make sure Terraform is installed by running the command – $ terraform -version
Now, here’s how to manage AWS Lambda functions:
The first step is to set up an IAM Role for your Lambda function and any policies that it requires. The exact policies required will depend on what your function does and what services it needs to access. Since this article is mainly focused on explaining how to manage Lambda functions via Terraform, we will keep the IAM Roles and Policies very basic.
Create main.tf and add IAM role
Let’s start by creating a main.tf file and inside the main.tf file, creating a role called Spacelift_Test_Lambda_Function_Role.
resource "aws_iam_role" "lambda_role" {
name = "Spacelift_Test_Lambda_Function_Role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
Add an IAM policy
After creating the IAM Role, let’s create an IAM Policy to manage the permissions associated with the role. As this is a basic application, we will be assigning the following permissions:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Add the following IAM Policy resource block to main.tf:
resource "aws_iam_policy" "iam_policy_for_lambda" {
name = "aws_iam_policy_for_terraform_aws_lambda_role"
path = "/"
description = "AWS IAM Policy for managing aws lambda role"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*",
"Effect": "Allow"
}
]
}
EOF
}
Attach IAM policy to IAM role
Now that we have created an IAM policy and IAM role for the Terraform managed AWS Lambda function, let’s attach both the IAM policy and IAM role to each other:
resource "aws_iam_role_policy_attachment" "attach_iam_policy_to_iam_role" {
role = aws_iam_role.lambda_role.name
policy_arn = aws_iam_policy.iam_policy_for_lambda.arn
}
Read more about Creating IAM Policies with Terraform.
Since the IAM Role and IAM policy have now been created, let’s call on the Python application that we will be running on AWS Lambda.
- Create a directory python parallel to main.tf.
- Inside the directory python, create a file index.py.
- Add the following python function to it:
def lambda_handler(event, context):
message = 'Hello {} !'.format(event['key1'])
return {
'message' : message
}
As in the previous step, we created a Python file index.py. Now, we need to create a ZIP file because aws_lambda_function
needs the code to be stored in a ZIP file before uploading it to AWS.
This ZIP file we are going to upload and submit this to the AWS Lambda function:
data "archive_file" "zip_the_python_code" {
type = "zip"
source_dir = "${path.module}/python/"
output_path = "${path.module}/python/hello-python.zip"
}
Alright. Now we have everything (IAM role, IAM policy, Python code) in place. Let’s write down the aws_lambda_function resource block:
resource "aws_lambda_function" "terraform_lambda_func" {
filename = "${path.module}/python/hello-python.zip"
function_name = "Spacelift_Test_Lambda_Function"
role = aws_iam_role.lambda_role.arn
handler = "index.lambda_handler"
runtime = "python3.8"
depends_on = [aws_iam_role_policy_attachment.attach_iam_policy_to_iam_role]
}
Here are a few things you should keep in mind while writing aws_lambda_function resource block:
- Runtime — You should mention the correction runtime that AWS Lambda will use to run your Lambda function. Currently, AWS Lambda supports Node.js, Python, Java, Ruby, Go, and NET.
- IAM role — Always mention the correct IAM role that you have created for the Lambda function.
- Depends_on — Mention the correct IAM policy attachment block where you have attached the IAM role to the IAM policy. This is a sanity check to make sure that the IAM Policy and IAM roles are in place before the lambda function is created.
Here is the complete main.tf file:
provider "aws" {
region = "eu-central-1"
}
resource "aws_iam_role" "lambda_role" {
name = "Spacelift_Test_Lambda_Function_Role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
resource "aws_iam_policy" "iam_policy_for_lambda" {
name = "aws_iam_policy_for_terraform_aws_lambda_role"
path = "/"
description = "AWS IAM Policy for managing aws lambda role"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*",
"Effect": "Allow"
}
]
}
EOF
}
resource "aws_iam_role_policy_attachment" "attach_iam_policy_to_iam_role" {
role = aws_iam_role.lambda_role.name
policy_arn = aws_iam_policy.iam_policy_for_lambda.arn
}
data "archive_file" "zip_the_python_code" {
type = "zip"
source_dir = "${path.module}/python/"
output_path = "${path.module}/python/hello-python.zip"
}
resource "aws_lambda_function" "terraform_lambda_func" {
filename = "${path.module}/python/hello-python.zip"
function_name = "Spacelift_Test_Lambda_Function"
role = aws_iam_role.lambda_role.arn
handler = "index.lambda_handler"
runtime = "python3.8"
depends_on = [aws_iam_role_policy_attachment.attach_iam_policy_to_iam_role]
}
Let’s run the following commands in sequence:
1. $ terraform init
2. $ terraform plan
Here is the remaining output of the terraform plan
:
3. $ terraform apply
Here is the remaining output of the terraform apply
:
Let’s head over to the AWS Console and do the test run. From the AWS dashboard, you can start by searching Lambda from the search bar:
Click on Lambda to see the Lambda function we have provisioned using Terraform:
Click on it, then use the Test button to run the Lambda function:
A new window will open where you need to specify the event name. As you can see in the picture below, we have assigned the event name MyCustomMessage and entered a custom message as key1.
Now, click on the Test tab and then on the orange Test Button:
As you can see, it has returned the message Hello Spacelift, which we have written inside the python file.
Here’s another example of how to define a simple AWS Lambda function in Terraform.
For this example, we will use a hello world Lambda function:
def lambda_handler(event, context):
return {
'statusCode': 200,
'body': 'Hello, World!'
}
I’ve saved this into a file called hello.py.
The first step we will do in our Terraform code is to define our provider and a role for the Lambda function:
provider "aws" {
region = "eu-west-1"
}
resource "aws_iam_role" "lambda_exec_role" {
name = "lambda_execution_role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Action = "sts:AssumeRole",
Principal = {
Service = "lambda.amazonaws.com"
},
Effect = "Allow"
}
]
})
}
resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
role = aws_iam_role.lambda_exec_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
The above role uses an AWSLambdaBasicExecutionRole policy that provides the necessary permissions to execute lambda functions and write logs to AmazonCloudwatch.
Next, we will define an archive_file datasource that creates the lambda archive for us:
data "archive_file" "lambda" {
type = "zip"
source_file = "hello.py"
output_path = "hello.zip"
}
In the end, we define the lambda function to use the role and the archive we’ve created above and specify the handler of our function in this format: function_name.lambda_handler. In addition, we are creating an aws_cloudwatch_log_group to get the logs of our Lambda execution.
resource "aws_lambda_function" "hello_world_lambda" {
function_name = "hello_world_lambda"
role = aws_iam_role.lambda_exec_role.arn
handler = "hello.lambda_handler"
runtime = "python3.8"
filename = data.archive_file.lambda.output_path
}
resource "aws_cloudwatch_log_group" "lambda_log_group" {
name = "/aws/lambda/hello_world_lambda"
retention_in_days = 14
}
Now, let’s apply the Terraform code:
terraform apply
Plan: 4 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
OpenTofu will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_cloudwatch_log_group.lambda_log_group: Creating...
aws_iam_role.lambda_exec_role: Creating...
aws_cloudwatch_log_group.lambda_log_group: Creation complete after 1s [id=/aws/lambda/hello_world_lambda]
aws_iam_role.lambda_exec_role: Creation complete after 1s [id=lambda_execution_role]
aws_iam_role_policy_attachment.lambda_basic_execution: Creating...
aws_lambda_function.hello_world_lambda: Creating...
aws_iam_role_policy_attachment.lambda_basic_execution: Creation complete after 1s [id=lambda_execution_role-20240909063234551900000001]
aws_lambda_function.hello_world_lambda: Still creating... [10s elapsed]
aws_lambda_function.hello_world_lambda: Creation complete after 15s [id=hello_world_lambda]
We are now ready to invoke the lambda function, so we will do that and save the output in a file called output.txt.
aws lambda invoke --function-name hello_world_lambda output.txt
Let’s check the output:
{"statusCode": 200, "body": "Hello, World!"}
We can also check the log event in CloudWatch:
There is an AWS Lambda Terraform module available in the Terraform registry. Let’s use this module to build something similar to what we’ve done in the previous example.
provider "aws" {
region = "eu-west-1"
}
module "lambda" {
source = "terraform-aws-modules/lambda/aws"
version = "7.8.1"
function_name = "hello"
description = "My awesome lambda function"
handler = "hello.lambda_handler"
runtime = "python3.8"
source_path = "./hello.py"
}
This will create almost the same resources as in our above case. The only difference is that the permissions are slightly different, and the archive is created using a null resource with a local-exec provisioner that runs a Python script.
Let’s run the code and see the output:
Enter a value: yes
module.lambda.local_file.archive_plan[0]: Creating...
module.lambda.local_file.archive_plan[0]: Creation complete after 0s [id=824e417df9f89264292831d39287ca8c0de20fe5]
module.lambda.null_resource.archive[0]: Creating...
module.lambda.null_resource.archive[0]: Provisioning with 'local-exec'...
module.lambda.null_resource.archive[0] (local-exec): Executing: ["python3" ".terraform/modules/lambda/package.py" "build" "--timestamp" "1725865297519310000" "builds/6a45bb0571c326179278cd9a7dafe3a10dc848ba2272c6219165a79678344616.plan.json"]
module.lambda.null_resource.archive[0] (local-exec): zip: creating 'builds/6a45bb0571c326179278cd9a7dafe3a10dc848ba2272c6219165a79678344616.zip' archive
module.lambda.null_resource.archive[0] (local-exec): zip: adding: hello.py
module.lambda.null_resource.archive[0] (local-exec): Created: builds/6a45bb0571c326179278cd9a7dafe3a10dc848ba2272c6219165a79678344616.zip
module.lambda.null_resource.archive[0]: Creation complete after 0s [id=938250342508072512]
module.lambda.aws_cloudwatch_log_group.lambda[0]: Creating...
module.lambda.aws_iam_role.lambda[0]: Creating...
module.lambda.aws_cloudwatch_log_group.lambda[0]: Creation complete after 0s [id=/aws/lambda/hello]
module.lambda.data.aws_iam_policy_document.logs[0]: Reading...
module.lambda.data.aws_iam_policy_document.logs[0]: Read complete after 0s [id=1947533982]
module.lambda.aws_iam_policy.logs[0]: Creating...
module.lambda.aws_iam_policy.logs[0]: Creation complete after 1s [id=arn:aws:iam::094873932114:policy/hello-logs]
module.lambda.aws_iam_role.lambda[0]: Creation complete after 1s [id=hello]
module.lambda.aws_iam_role_policy_attachment.logs[0]: Creating...
module.lambda.aws_iam_role_policy_attachment.logs[0]: Creation complete after 0s [id=hello-20240909070216057400000001]
module.lambda.aws_lambda_function.this[0]: Creating...
module.lambda.aws_lambda_function.this[0]: Still creating... [10s elapsed]
module.lambda.aws_lambda_function.this[0]: Creation complete after 14s [id=hello]
Now we can invoke the function to ensure that everything is working properly:
aws lambda invoke --function-name hello output.txt
{"statusCode": 200, "body": "Hello World!"}
Managing AWS Lambda: Terraform vs CloudFormation
Both Terraform and CloudFormation can manage AWS Lambda functions, but they differ in their approach and flexibility. Terraform being cloud-agnostic, can manage resources across multiple cloud providers, while CloudFormation is specifically designed for AWS. Terraform uses its own HashiCorp Configuration Language (HCL) for defining infrastructure, which many find more flexible and readable than CloudFormation’s JSON or YAML.
Terraform is often praised for its simpler syntax and better modularity, while CloudFormation offers native support for AWS services, which can be advantageous for users who require the latest updates and features from AWS.
For this part, we will create a GitHub repository based on the code we initially built. The example GitHub repository is here.
Now, let’s go to our Spacelift account and create a stack:
Add a name to your stack, select a Space, optionally add Labels and a Description, and then click on continue.
In the Connect to source code, select your VCS provider and the repository containing the code.
In the Choose vendor step you can accept the defaults: use the latest FOSS version of Terraform.
At this point, the stack is created, but we should also consider how to authenticate to AWS. For that, I will attach a cloud integration. If you don’t know how to create a cloud integration for AWS, check out this guide.
Add your integration, select both read and write and click on Attach. Then you can click on go to summary and confirm.
Let’s trigger a run on our stack to create the resources:
After the plan finishes, we can check what will be created in a human-readable format:
Let’s confirm the run and wait for the resources to be created:
Now, we can invoke the function using a task:
As you can see, the function was executed successfully and the message has been printed.
This article is meant to familiarize you with using Terraform to manage AWS Lambda functions. Although the article uses Python, you can satisfy your business needs using other programming languages such as GO, Java, Ruby, or .NET. The core concepts of setting AWS Lambda with Terraform remain the same regardless of your language choice.
If you want to elevate your Terraform management, create a free account for Spacelift today or book a demo with one of our engineers.
Note: New versions of Terraform are placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that expands on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6.
Terraform Management Made Easy
Spacelift effectively manages Terraform state, more complex workflows, supports policy as code, programmatic configuration, context sharing, drift detection, resource visualization and includes many more features.