Terraform AWS Lambda
Introduction
AWS Lambda is a serverless compute service that lets you run code without provisioning or managing servers. With Terraform, we can define, deploy, and manage Lambda functions using infrastructure as code. This means you can version control your Lambda functions and deploy them consistently across environments.
In this tutorial, we'll learn how to:
- Create Lambda functions using Terraform
- Configure Lambda permissions and roles
- Set up Lambda triggers and environment variables
- Deploy and update Lambda functions
- Implement best practices for managing Lambda with Terraform
Prerequisites
Before you begin, ensure you have:
- Terraform installed (v1.0.0+)
- AWS account with appropriate permissions
- AWS CLI configured with your credentials
- Basic understanding of AWS services and Terraform concepts
Getting Started with Terraform and AWS Lambda
Let's start by setting up a basic Terraform configuration to create an AWS Lambda function.
Project Structure
Create the following file structure for your project:
terraform-aws-lambda/
├── main.tf
├── variables.tf
├── outputs.tf
├── lambda/
│ └── hello.js
Provider Configuration
First, let's configure the AWS provider in main.tf
:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
required_version = ">= 1.0.0"
}
provider "aws" {
region = var.aws_region
}
Variables Definition
Create a variables.tf
file to define variables:
variable "aws_region" {
description = "AWS region for all resources"
type = string
default = "us-east-1"
}
variable "function_name" {
description = "Name of the Lambda function"
type = string
default = "terraform-lambda-example"
}
variable "lambda_runtime" {
description = "Runtime for Lambda function"
type = string
default = "nodejs16.x"
}
Creating Your First Lambda Function with Terraform
Lambda Function Code
Create a simple Node.js Lambda function in the lambda/hello.js
file:
exports.handler = async (event) => {
console.log('Event received:', JSON.stringify(event, null, 2));
const response = {
statusCode: 200,
body: JSON.stringify({
message: 'Hello from Terraform-managed Lambda!',
input: event,
}),
};
return response;
};
Lambda IAM Role
Before creating the Lambda function, we need to create an IAM role that gives Lambda permission to execute and log to CloudWatch:
# IAM role for Lambda
resource "aws_iam_role" "lambda_role" {
name = "${var.function_name}-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
},
]
})
}
# Attach the basic Lambda execution policy to the role
resource "aws_iam_role_policy_attachment" "lambda_basic" {
role = aws_iam_role.lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
Lambda Function Resource
Now, let's define the Lambda function resource in main.tf
:
# Archive file for Lambda code
data "archive_file" "lambda_zip" {
type = "zip"
source_file = "${path.module}/lambda/hello.js"
output_path = "${path.module}/lambda/hello.zip"
}
# Lambda function
resource "aws_lambda_function" "hello_lambda" {
filename = data.archive_file.lambda_zip.output_path
function_name = var.function_name
role = aws_iam_role.lambda_role.arn
handler = "hello.handler"
runtime = var.lambda_runtime
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
environment {
variables = {
ENVIRONMENT = "development"
}
}
tags = {
Name = var.function_name
Environment = "development"
Terraform = "true"
}
}
Outputs
Create an outputs.tf
file to expose important information about your Lambda function:
output "function_name" {
description = "Name of the Lambda function"
value = aws_lambda_function.hello_lambda.function_name
}
output "function_arn" {
description = "ARN of the Lambda function"
value = aws_lambda_function.hello_lambda.arn
}
output "invoke_arn" {
description = "Invoke ARN of the Lambda function"
value = aws_lambda_function.hello_lambda.invoke_arn
}
Deploying and Testing the Lambda Function
Deploy with Terraform
Deploy your Lambda function with these commands:
terraform init
terraform plan
terraform apply
After you apply the changes, Terraform will output the Lambda function's details, including its name and ARN.
Testing the Lambda Function
You can test the function using the AWS CLI:
aws lambda invoke \
--function-name $(terraform output -raw function_name) \
--payload '{"key": "value"}' \
response.json
Check the response:
cat response.json
Adding API Gateway Trigger
Let's enhance our Lambda function by adding an API Gateway trigger:
# API Gateway REST API
resource "aws_api_gateway_rest_api" "api" {
name = "${var.function_name}-api"
description = "API Gateway for Lambda function"
}
# API Gateway resource and method
resource "aws_api_gateway_resource" "resource" {
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_rest_api.api.root_resource_id
path_part = "hello"
}
resource "aws_api_gateway_method" "method" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.resource.id
http_method = "GET"
authorization_type = "NONE"
}
# Integration between API Gateway and Lambda
resource "aws_api_gateway_integration" "integration" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.resource.id
http_method = aws_api_gateway_method.method.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.hello_lambda.invoke_arn
}
# Permission for API Gateway to invoke Lambda
resource "aws_lambda_permission" "api_gateway" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.hello_lambda.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/${aws_api_gateway_method.method.http_method}${aws_api_gateway_resource.resource.path}"
}
# API Gateway deployment
resource "aws_api_gateway_deployment" "deployment" {
depends_on = [
aws_api_gateway_integration.integration
]
rest_api_id = aws_api_gateway_rest_api.api.id
stage_name = "dev"
# Force new deployment when integration changes
triggers = {
redeployment = sha1(jsonencode([
aws_api_gateway_resource.resource,
aws_api_gateway_method.method,
aws_api_gateway_integration.integration,
]))
}
lifecycle {
create_before_destroy = true
}
}
# Output the API URL
output "api_url" {
value = "${aws_api_gateway_deployment.deployment.invoke_url}${aws_api_gateway_resource.resource.path}"
}
Advanced Lambda Configuration
Adding CloudWatch Log Group with Custom Retention
resource "aws_cloudwatch_log_group" "lambda_logs" {
name = "/aws/lambda/${aws_lambda_function.hello_lambda.function_name}"
retention_in_days = 14
}
Configure Lambda Memory and Timeout
You can optimize your Lambda's performance by adjusting memory and timeout settings:
resource "aws_lambda_function" "hello_lambda" {
# ... existing configuration ...
memory_size = 256
timeout = 30
}
Lambda Layers for Code Reuse
Lambda Layers allow you to package and reuse common dependencies:
resource "aws_lambda_layer_version" "dependencies_layer" {
layer_name = "shared-dependencies"
compatible_runtimes = [var.lambda_runtime]
filename = "${path.module}/layers/dependencies.zip"
source_code_hash = filebase64sha256("${path.module}/layers/dependencies.zip")
}
resource "aws_lambda_function" "hello_lambda" {
# ... existing configuration ...
layers = [aws_lambda_layer_version.dependencies_layer.arn]
}
Working with Lambda Environment Variables
Environment variables allow you to pass runtime configuration to your Lambda function:
resource "aws_lambda_function" "hello_lambda" {
# ... existing configuration ...
environment {
variables = {
ENVIRONMENT = var.environment
DB_HOST = var.db_host
API_KEY = var.api_key
DEBUG_MODE = "true"
}
}
}
In your variables.tf
:
variable "environment" {
description = "Deployment environment"
type = string
default = "development"
}
variable "db_host" {
description = "Database host"
type = string
default = "db.example.com"
}
variable "api_key" {
description = "API key for external service"
type = string
sensitive = true
}
Setting Up Lambda Event Sources
S3 Event Trigger
resource "aws_s3_bucket" "trigger_bucket" {
bucket = "lambda-trigger-bucket-${random_pet.suffix.id}"
}
resource "aws_s3_bucket_notification" "bucket_notification" {
bucket = aws_s3_bucket.trigger_bucket.id
lambda_function {
lambda_function_arn = aws_lambda_function.hello_lambda.arn
events = ["s3:ObjectCreated:*"]
filter_prefix = "uploads/"
filter_suffix = ".json"
}
}
resource "aws_lambda_permission" "s3_invoke" {
statement_id = "AllowS3Invoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.hello_lambda.function_name
principal = "s3.amazonaws.com"
source_arn = aws_s3_bucket.trigger_bucket.arn
}
CloudWatch Event Trigger (EventBridge)
resource "aws_cloudwatch_event_rule" "schedule" {
name = "${var.function_name}-schedule"
description = "Schedule for Lambda Function"
schedule_expression = "rate(1 hour)"
}
resource "aws_cloudwatch_event_target" "lambda_target" {
rule = aws_cloudwatch_event_rule.schedule.name
target_id = "TriggerLambda"
arn = aws_lambda_function.hello_lambda.arn
}
resource "aws_lambda_permission" "cloudwatch_invoke" {
statement_id = "AllowCloudWatchInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.hello_lambda.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.schedule.arn
}
Function Deployment Strategies
Using Lambda Aliases and Versions
Versions and aliases help manage Lambda deployments:
# Publish a version of the Lambda function
resource "aws_lambda_function_version" "latest" {
function_name = aws_lambda_function.hello_lambda.function_name
qualifier = aws_lambda_function.hello_lambda.version
}
# Create aliases for different environments
resource "aws_lambda_alias" "dev" {
name = "dev"
function_name = aws_lambda_function.hello_lambda.function_name
function_version = aws_lambda_function_version.latest.version
}
resource "aws_lambda_alias" "prod" {
name = "prod"
function_name = aws_lambda_function.hello_lambda.function_name
function_version = aws_lambda_function_version.latest.version
# For production, we could implement a deployment strategy with weighted routing
routing_config {
additional_version_weights = {
"1" = 0.1 # Route 10% of traffic to version 1
}
}
}
Best Practices
Creating a Module for Reusable Lambda Functions
For larger projects, create a reusable Terraform module:
# modules/lambda/main.tf
resource "aws_lambda_function" "this" {
function_name = var.function_name
filename = var.filename
source_code_hash = var.source_code_hash
handler = var.handler
runtime = var.runtime
role = aws_iam_role.lambda_role.arn
memory_size = var.memory_size
timeout = var.timeout
environment {
variables = var.environment_variables
}
tags = var.tags
}
# IAM role and policies
resource "aws_iam_role" "lambda_role" {
name = "${var.function_name}-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
},
]
})
}
resource "aws_cloudwatch_log_group" "lambda_logs" {
name = "/aws/lambda/${aws_lambda_function.this.function_name}"
retention_in_days = var.log_retention_days
}
resource "aws_iam_policy" "lambda_logging" {
name = "${var.function_name}-logging"
description = "IAM policy for logging from a lambda"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Effect = "Allow"
Resource = "${aws_cloudwatch_log_group.lambda_logs.arn}:*"
},
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)