Table of Contents

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

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.