TerraformPilot

Cloud Computing

Deploy an AWS Lambda Function with Terraform

Step-by-step guide to deploying serverless AWS Lambda functions using Terraform with IAM roles, API Gateway triggers, and CloudWatch logging.

LLuca Berton1 min read

Introduction

#

AWS Lambda lets you run code without provisioning servers. Combined with Terraform, you can version-control your entire serverless infrastructure. This guide walks through creating a production-ready Lambda function with proper IAM permissions, API Gateway integration, and monitoring.

Prerequisites

#
  • AWS account with appropriate permissions
  • Terraform >= 1.5 installed
  • Basic knowledge of Python or Node.js

Project Structure

#
lambda-project/
├── main.tf
├── variables.tf
├── outputs.tf
├── lambda/
│   └── handler.py
└── terraform.tfvars

Creating the Lambda Function Code

#
# lambda/handler.py
import json
import logging
 
logger = logging.getLogger()
logger.setLevel(logging.INFO)
 
def lambda_handler(event, context):
    logger.info(f"Received event: {json.dumps(event)}")
 
    return {
        'statusCode': 200,
        'headers': {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*'
        },
        'body': json.dumps({
            'message': 'Hello from Terraform-managed Lambda!',
            'input': event
        })
    }

Terraform Configuration

#

IAM Role for Lambda

#
resource "aws_iam_role" "lambda_exec" {
  name = "lambda-exec-role"
 
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}
 
resource "aws_iam_role_policy_attachment" "lambda_basic" {
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
  role       = aws_iam_role.lambda_exec.name
}

Lambda Function Resource

#
data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = "${path.module}/lambda"
  output_path = "${path.module}/lambda.zip"
}
 
resource "aws_lambda_function" "api" {
  filename         = data.archive_file.lambda_zip.output_path
  function_name    = var.function_name
  role             = aws_iam_role.lambda_exec.arn
  handler          = "handler.lambda_handler"
  runtime          = "python3.12"
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256
  timeout          = 30
  memory_size      = 256
 
  environment {
    variables = {
      ENVIRONMENT = var.environment
      LOG_LEVEL   = "INFO"
    }
  }
 
  tracing_config {
    mode = "Active"
  }
 
  tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

API Gateway Integration

#
resource "aws_apigatewayv2_api" "lambda_api" {
  name          = "${var.function_name}-api"
  protocol_type = "HTTP"
 
  cors_configuration {
    allow_origins = ["*"]
    allow_methods = ["GET", "POST", "OPTIONS"]
    allow_headers = ["Content-Type"]
    max_age       = 300
  }
}
 
resource "aws_apigatewayv2_stage" "default" {
  api_id      = aws_apigatewayv2_api.lambda_api.id
  name        = "$default"
  auto_deploy = true
 
  access_log_settings {
    destination_arn = aws_cloudwatch_log_group.api_gw.arn
    format = jsonencode({
      requestId      = "$context.requestId"
      ip             = "$context.identity.sourceIp"
      requestTime    = "$context.requestTime"
      httpMethod     = "$context.httpMethod"
      routeKey       = "$context.routeKey"
      status         = "$context.status"
      protocol       = "$context.protocol"
      responseLength = "$context.responseLength"
    })
  }
}
 
resource "aws_apigatewayv2_integration" "lambda" {
  api_id             = aws_apigatewayv2_api.lambda_api.id
  integration_type   = "AWS_PROXY"
  integration_uri    = aws_lambda_function.api.invoke_arn
  integration_method = "POST"
}
 
resource "aws_apigatewayv2_route" "default" {
  api_id    = aws_apigatewayv2_api.lambda_api.id
  route_key = "GET /"
  target    = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}
 
resource "aws_lambda_permission" "api_gw" {
  statement_id  = "AllowAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.api.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_apigatewayv2_api.lambda_api.execution_arn}/*/*"
}

CloudWatch Logging

#
resource "aws_cloudwatch_log_group" "lambda" {
  name              = "/aws/lambda/${aws_lambda_function.api.function_name}"
  retention_in_days = 14
}
 
resource "aws_cloudwatch_log_group" "api_gw" {
  name              = "/aws/apigateway/${aws_apigatewayv2_api.lambda_api.name}"
  retention_in_days = 14
}

Variables and Outputs

#
# variables.tf
variable "function_name" {
  default = "my-api-function"
}
 
variable "environment" {
  default = "production"
}
 
# outputs.tf
output "api_endpoint" {
  value = aws_apigatewayv2_stage.default.invoke_url
}
 
output "function_name" {
  value = aws_lambda_function.api.function_name
}

Deployment

#
terraform init
terraform plan
terraform apply

After deployment, Terraform outputs the API endpoint URL. Test it with:

curl $(terraform output -raw api_endpoint)

Best Practices

#
  1. Use source_code_hash to detect code changes automatically
  2. Set appropriate timeout and memory — start small, scale up based on metrics
  3. Enable X-Ray tracing for debugging distributed systems
  4. Set log retention to control CloudWatch costs
  5. Use environment variables for configuration, never hardcode secrets
  6. Tag everything for cost tracking and organization

Hands-On Courses

#

Learn by doing with interactive courses on CopyPasteLearn:

Conclusion

#

Terraform makes Lambda deployments repeatable and version-controlled. By combining Lambda with API Gateway, IAM, and CloudWatch through Terraform, you get a complete serverless stack that's easy to maintain, replicate across environments, and roll back when needed.

#Terraform#AWS#Serverless#Infrastructure as Code#Cloud Computing

Share this article