Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: March 27, 2025
Terraform is an Infrastructure-as-Code tool that enables the creation, modification, and improvement of infrastructure. On the other hand, AWS Lambda is a serverless computing service that can run code without provisioning or managing servers. It automatically scales applications by running code responding to each trigger, scaling precisely with the workload.
Therefore, by combining the Lambda serverless computing features with the Terraform infrastructure as code (IAC) approach, developers can obtain versioned infrastructure configurations alongside application code, replicate identical environments with the same configuration, automate infrastructure changes within the CI/CD pipeline, and track the state of the infrastructure.
In this tutorial, we’ll cover how to set up AWS Lambda functions in Terraform. All commands have been tested on Ubuntu 22.04 with the AWS CLI version 1.37.11.
Before we begin setting up AWS Lambda with Terraform, we must verify and ensure that some prerequisites are available:
$ terraform --version
Terraform v1.9.8
on linux_amd64
$ aws --version
aws-cli/1.37.11 Python/3.10.12 Linux/5.15.0-82-generic botocore/1.36.11
With all of these available, some basic prior knowledge of AWS cloud and Terraform is required.
While we can use the administrative credentials already configured in the AWS CLI, using dedicated IAM users and following the principle of least privilege are AWS best practices.
To enable Terraform to manage AWS resources securely, we create a dedicated IAM user named terraform-user with limited permissions:
$ aws iam create-user --user-name terraform-user
{
"User": {
"Path": "/",
"UserName": "terraform-user",
"UserId": "AIDA4T4OCLX5W42FFGY62",
"Arn": "arn:aws:iam::<ACCOUNT_ID>:user/terraform-user",
"CreateDate": "2025-02-14T15:49:05Z"
}
}
Next, we set up a directory named aws-lambda-terraform to store some demo files:
$ mkdir ~/aws-lambda-terraform && cd ~/aws-lambda-terraform
Inside this directory, we create a JSON file named terraform-policy. This JSON file contains the necessary permissions required by the terraform-user to interact with AWS Lambda:
$ cat ~/aws-lambda-terraform/terraform-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:CreateFunction",
"lambda:UpdateFunctionCode",
"lambda:DeleteFunction",
"lambda:AddPermission",
"lambda:RemovePermission",
"lambda:GetFunction",
"lambda:ListVersionsByFunction",
"lambda:GetFunctionCodeSigningConfig",
"lambda:GetPolicy",
"lambda:InvokeFunction",
"events:PutRule",
"events:PutTargets",
"events:DeleteRule",
"events:RemoveTargets",
"events:DescribeRule",
"events:ListRules",
"events:ListTagsForResource",
"events:ListTargetsByRule",
"events:ListTargetsByRule",
"iam:CreateRole",
"iam:PutRolePolicy",
"iam:AttachRolePolicy",
"iam:DeleteRole",
"iam:DetachRolePolicy",
"iam:GetRole",
"iam:PassRole",
"iam:ListRolePolicies",
"iam:ListAttachedRolePolicies",
"iam:ListInstanceProfilesForRole",
"logs:CreateLogGroup",
"logs:DescribeLogGroups"
],
"Resource": [
"arn:aws:lambda:us-east-1:<ACCOUNT_ID>:function:hello_lambda",
"arn:aws:events:us-east-1:<ACCOUNT_ID>:rule/*",
"arn:aws:iam::<ACCOUNT_ID>:role/lambda_exec_role",
"arn:aws:logs:us-east-1:<ACCOUNT_ID>:log-group:*"
]
}
]
}
This policy allows complete management of a specific Lambda function (hello_lambda), its associated CloudWatch Events rules, an IAM execution role (lambda_exec_role), and related CloudWatch Logs within the AWS account and region (us-east-1).
Next, we create the policy using the CLI:
$ aws iam create-policy \
--policy-name TerraformLambdaPolicy \
--policy-document file://terraform-policy.json
{
"Policy": {
"PolicyName": "TerraformLambdaPolicy",
"PolicyId": "ANPA4T4OCLX5U4EKMVY2C",
"Arn": "arn:aws:iam::<ACCOUNT_ID>:policy/TerraformLambdaPolicy",
"Path": "/",
"DefaultVersionId": "v1",
"AttachmentCount": 0,
"PermissionsBoundaryUsageCount": 0,
"IsAttachable": true,
"CreateDate": "2025-02-14T15:51:06Z",
"UpdateDate": "2025-02-14T15:51:06Z"
}
}
Once successfully created, we can attach this policy directly to the terraform-user:
$ aws iam attach-user-policy \
--user-name terraform-user \
--policy-arn "arn:aws:iam::<ACCOUNT_ID>:policy/TerraformLambdaPolicy"
Next, we create the access key for the terraform-user:
$ aws iam create-access-key --user-name terraform-user
{
"AccessKey": {
"UserName": "terraform-user",
"AccessKeyId": "AKIA4T4OCLX5VF63ZO4K",
"Status": "Active",
"SecretAccessKey": <SECRET_ACCESS_KEY>,
"CreateDate": "2025-02-14T15:58:24Z"
}
}
Using the AccessKeyId and SecretAccessKey obtained above, we can set these as the default AWS credentials via aws configure:
$ aws configure
AWS Access Key ID [****************ZO4K]:
AWS Secret Access Key [****************LRvO]:
Default region name [us-east-1]:
Default output format [json]:
Hence, we’ve successfully set up the proper IAM permissions for Terraform to interact with AWS Lambda using the principle of least privilege.
Firstly, let’s set up the project structure in the aws-lambda-terraform directory:
aws-lambda-terraform/
├── main.tf # Main Terraform configuration
├── outputs.tf # Output definitions
└── lambda/
└── hello_lambda.py # Lambda function code
In the main.tf file, we configure the AWS Terraform provider:
$ cat ~/aws-lambda-terraform/main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
The above configuration sets up AWS as the infrastructure provider and targets the us-east-1 region as the location for all resources.
Based on the project structure, the Lambda function goes into hello_lambda.py. When invoked, this Python Lambda function returns a JSON response with a greeting and the environment name:
$ cat ~/aws-lambda-terraform/lambda/hello-lambda.py
import json
import os
def lambda_handler(event, context):
return {
'statusCode': 200,
'body': json.dumps({
'message': 'Hello from Lambda!',
'environment': os.getenv('ENV', 'default')
})
}
The function follows the AWS Lambda standard and takes event and context as parameters. It returns an API gateway-compatible response, a JSON-formatted body, and a 200 status code.
An IAM role must grant the Lambda function permissions to execute, write logs to CloudWatch, and access other AWS services. Let’s set up the IAM role for the Lambda function in our main.tf file:
$ cat ~/aws-lamda-terraform/main.tf
...
resource "aws_iam_role" "lambda_exec" {
name = "lambda_exec_role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Sid = ""
Principal = {
Service = "lambda.amazonaws.com"
}
},
]
})
}
resource "aws_iam_role_policy_attachment" "lambda_basic" {
role = aws_iam_role.lambda_exec.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
Without this IAM configuration, the Lambda function would not have permission to execute or write logs.
Furthermore, to deploy the function to AWS Lambda, we must package it as a zipped archive. Using the archive_file data source, we zip the Python function in the main.tf file:
$ cat ~/aws-lamda-terraform/main.tf
...
data "archive_file" "lambda_zip" {
type = "zip"
source_dir = "${path.module}/lambda"
output_path = "${path.module}/lambda/lambda_function.zip"
}
Next, we create the Lambda function resource with the aws_lambda_function resource:
$ cat ~/aws-lamda-terraform/main.tf
...
resource "aws_lambda_function" "hello_lambda" {
filename = data.archive_file.lambda_zip.output_path
function_name = "hello_lambda"
role = aws_iam_role.lambda_exec.arn
handler = "hello_lambda.lambda_handler"
runtime = "python3.9"
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
environment {
variables = {
ENV = "test"
}
}
}
As mentioned earlier, we defined the Lambda function as hello_lambda. It uses the previously created IAM execution role and then uploads the Python Lambda function as a ZIP file from the archive_file data source. Additionally, a source hash tracks any code changes.
After deployment, let’s set the function name and ARN as outputs on the command line for verification and reference purposes. We can do this in the outputs.tf file:
$ cat ~/aws-lamda-terraform/outputs.tf
output "lambda_function_name" {
value = aws_lambda_function.hello_lambda.function_name
}
output "lambda_arn" {
value = aws_lambda_function.hello_lambda.arn
}
Thus, we defined the necessary resources for the Lambda function.
At this point, we can deploy the above Lambda configurations with Terraform. In the CLI, let’s run terraform init to initialize the directory and install the necessary providers:
$ terraform init
Initializing the backend...
Initializing provider plugins...
...
We can preview the changes to be made with the terraform plan command:
$ terraform plan
terraform plan
data.archive_file.lambda_zip: Reading...
data.archive_file.lambda_zip: Read complete after 0s [id=4e1ee1aebee0d44e6bb7c48d9044ce11d7cf7517]
Terraform used the selected providers to generate the following execution plan. Resource actions are
indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_iam_role.lambda_exec will be created
+ resource "aws_iam_role" "lambda_exec" {
+ arn = (known after apply)
...
# aws_iam_role_policy_attachment.lambda_basic will be created
+ resource "aws_iam_role_policy_attachment" "lambda_basic" {
+ id = (known after apply)
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
+ role = "lambda_exec_role"
}
# aws_lambda_function.hello_lambda will be created
+ resource "aws_lambda_function" "hello_lambda" {
...
Plan: 3 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ lambda_arn = (known after apply)
+ lambda_function_name = "hello_lambda"
Finally, if the resulting changes are expected, we apply the configuration with terraform apply:
$ terraform apply
...
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Outputs:
lambda_arn = "arn:aws:lambda:us-east-1:<ACCOUNT_ID>:function:hello_lambda"
lambda_function_name = "hello_lambda"
As configured earlier, we can see the lambda_arn and lambda_function_name output once the application is complete.
Let’s test the Lambda function directly from the CLI. Since the configured terraform-user has the lambda:InvokeFunction permission, we can invoke the Lambda function from the CLI:
$ aws lambda invoke \
--function-name hello_lambda \
--region us-east-1 \
output.txt
The command triggers the Lambda function named hello_lambda and saves the response to an output.txt file. If executed successfully, the output should have a status code of 200:
$ cat output.txt
{"statusCode": 200, "body": "{\"message\": \"Hello from Lambda!\", \"environment\": \"test\"}"}
Additionally, we can get the execution logs from CloudWatch:
$ aws lambda invoke \
--function-name hello_lambda \
--region us-east-1 \
--log-type Tail \
output.txt | jq -r .LogResult | base64 --decode
START RequestId: a1326f4c-1505-48a7-a772-fa09ff982f39 Version: $LATEST
END RequestId: a1326f4c-1505-48a7-a772-fa09ff982f39
REPORT RequestId: a1326f4c-1505-48a7-a772-fa09ff982f39 Duration: 4.27 ms Billed Duration: 5 ms Memory Size: 128 MB Max Memory Used: 30 MB Init Duration: 79.31 ms
So, we successfully deployed and tested a Lambda function with Terraform.
Notably, a popular use case for AWS Lambda is executing a function responding to specific events called triggers. They enable Lambda functions to be event-driven, meaning they only run when needed, a core principle of serverless computing.
For instance, Amazon S3 (object creation and deletion), Amazon DynamoDB (table updates), Amazon API Gateway (API calls), Amazon SNS (message publishing), and Amazon EventBridge (scheduled events and state changes) trigger Lambda functions with their generated events.
Hence, let’s improve the Terraform configuration to set up a scheduled trigger with EventBridge for the existing Lambda function.
Firstly, let’s add a CloudWatch Event Rule resource in main.tf that schedules a trigger named scheduled_lambda_trigger to run every 5 minutes, acting like a cron job:
$ cat ~/aws-lamda-terraform/main.tf
...
resource "aws_cloudwatch_event_rule" "schedule_lambda" {
name = "scheduled_lambda_trigger"
description = "Triggers Lambda every 5 minutes"
schedule_expression = "rate(5 minutes)"
}
Then, we link the scheduled trigger to the Lambda function using the CloudWatch event target resource:
$ cat ~/aws-lamda-terraform/main.tf
resource "aws_cloudwatch_event_target" "lambda_target" {
rule = aws_cloudwatch_event_rule.schedule_lambda.name
target_id = "lambda_target"
arn = aws_lambda_function.hello_lambda.arn
}
Finally, we grant permissions to CloudWatch Events to invoke the Lambda function using the Lambda permission resource:
$ cat ~/aws-lamda-terraform/main.tf
resource "aws_lambda_permission" "allow_cloudwatch" {
statement_id = "AllowExecutionFromCloudWatch"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.hello_lambda.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.schedule_lambda.arn
}
Hence, the hello_lambda function automatically runs every five minutes with this configuration. Notably, each execution generates output similar to the one we saw earlier in output.txt.
So, let’s apply the trigger with terraform apply:
$ terraform apply
Once applied, we can check if scheduled_lambda_trigger is enabled:
$ aws events describe-rule --name scheduled_lambda_trigger
{
"Name": "scheduled_lambda_trigger",
"Arn": "arn:aws:events:us-east-1:<ACCOUNT_ID>:rule/scheduled_lambda_trigger",
"ScheduleExpression": "rate(5 minutes)",
"State": "ENABLED",
"Description": "Triggers Lambda every 5 minutes",
"EventBusName": "default",
"CreatedBy": "<ACCOUNT_ID>"
}
As seen above, the state is set to ENABLED, implying the trigger should be active.
Let’s also verify that the trigger target is the hello_lambda function:
$ aws events list-targets-by-rule --rule scheduled_lambda_trigger
{
"Targets": [
{
"Id": "lambda_target",
"Arn": "arn:aws:lambda:us-east-1:<ACCOUNT_ID>:function:hello_lambda"
}
]
}
Again, the output shows the hello_lambda Id and Arn in the Targets field for the trigger.
Since we confirmed the trigger creation and activation, we can check the Lambda function dashboard in the AWS console to see the number of invocations it made:
As seen above, the Timestamp indicates the Lambda function gets invoked every five minutes.
In this article, we learned how to set up an AWS Lambda function in Terraform.
We went from deploying a simple Python Lambda function to creating Lambda function triggers, using EventBridge as an example, all done with Terraform.