Integrating AWS Lambda Functions with API Gateway using Terraform

Learn how to create and deploy serverless APIs by integrating AWS Lambda functions with API Gateway using Terraform

Integrating AWS Lambda Functions with API Gateway using Terraform

Serverless architectures allow you to build and run applications without managing servers. This guide demonstrates how to create and deploy serverless APIs by integrating AWS Lambda with API Gateway using Terraform.

Video Tutorial

Learn more about managing AWS Lambda and API Gateway with Terraform in this comprehensive video tutorial:

Prerequisites

  • AWS CLI configured with appropriate permissions
  • Terraform installed (version 1.0.0 or later)
  • Basic understanding of Lambda and API Gateway
  • Node.js or Python for Lambda functions

Project Structure

serverless-terraform/
├── main.tf
├── variables.tf
├── outputs.tf
├── lambda.tf
├── api_gateway.tf
├── iam.tf
├── src/
│   └── hello-world/
│       ├── index.js
│       └── package.json
└── terraform.tfvars

Lambda Function Setup

Create src/hello-world/index.js:

exports.handler = async (event) => {
    const name = event.queryStringParameters?.name || 'World';
    
    return {
        statusCode: 200,
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            message: `Hello, ${name}!`,
            timestamp: new Date().toISOString()
        })
    };
};

Create lambda.tf:

# Archive the Lambda function code
data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = "${path.module}/src/hello-world"
  output_path = "${path.module}/dist/hello-world.zip"
}

# Lambda function
resource "aws_lambda_function" "hello_world" {
  filename         = data.archive_file.lambda_zip.output_path
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256
  function_name    = "${var.project_name}-hello-world"
  role            = aws_iam_role.lambda.arn
  handler         = "index.handler"
  runtime         = "nodejs18.x"

  environment {
    variables = {
      ENVIRONMENT = var.environment
    }
  }

  tags = {
    Name        = "${var.project_name}-hello-world"
    Environment = var.environment
  }
}

# Lambda permission for API Gateway
resource "aws_lambda_permission" "api_gateway" {
  statement_id  = "AllowAPIGatewayInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.hello_world.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_apigatewayv2_api.main.execution_arn}/*/*"
}

API Gateway Configuration

Create api_gateway.tf:

# HTTP API Gateway
resource "aws_apigatewayv2_api" "main" {
  name          = "${var.project_name}-http-api"
  protocol_type = "HTTP"

  cors_configuration {
    allow_headers = ["Content-Type", "Authorization"]
    allow_methods = ["GET", "POST", "PUT", "DELETE"]
    allow_origins = ["*"]
    max_age      = 300
  }

  tags = {
    Name        = "${var.project_name}-http-api"
    Environment = var.environment
  }
}

# API Gateway stage
resource "aws_apigatewayv2_stage" "main" {
  api_id      = aws_apigatewayv2_api.main.id
  name        = var.environment
  auto_deploy = true

  access_log_settings {
    destination_arn = aws_cloudwatch_log_group.api_gateway.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"
      responseTime  = "$context.responseLatency"
      integrationError = "$context.integrationErrorMessage"
    })
  }

  tags = {
    Name        = "${var.project_name}-${var.environment}-stage"
    Environment = var.environment
  }
}

# API Gateway routes
resource "aws_apigatewayv2_route" "hello_world" {
  api_id    = aws_apigatewayv2_api.main.id
  route_key = "GET /hello"
  target    = "integrations/${aws_apigatewayv2_integration.hello_world.id}"
}

# Lambda integration
resource "aws_apigatewayv2_integration" "hello_world" {
  api_id                 = aws_apigatewayv2_api.main.id
  integration_type       = "AWS_PROXY"
  integration_uri        = aws_lambda_function.hello_world.invoke_arn
  payload_format_version = "2.0"
}

# CloudWatch Log Group
resource "aws_cloudwatch_log_group" "api_gateway" {
  name              = "/aws/api-gateway/${var.project_name}"
  retention_in_days = 30

  tags = {
    Name        = "${var.project_name}-api-gateway-logs"
    Environment = var.environment
  }
}

IAM Configuration

Create iam.tf:

# Lambda execution role
resource "aws_iam_role" "lambda" {
  name = "${var.project_name}-lambda-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })

  tags = {
    Name        = "${var.project_name}-lambda-role"
    Environment = var.environment
  }
}

# Lambda basic execution policy
resource "aws_iam_role_policy_attachment" "lambda_basic" {
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
  role       = aws_iam_role.lambda.name
}

# API Gateway logging role
resource "aws_iam_role" "api_gateway" {
  name = "${var.project_name}-api-gateway-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "apigateway.amazonaws.com"
        }
      }
    ]
  })

  tags = {
    Name        = "${var.project_name}-api-gateway-role"
    Environment = var.environment
  }
}

# API Gateway logging policy
resource "aws_iam_role_policy" "api_gateway_logging" {
  name = "${var.project_name}-api-gateway-logging"
  role = aws_iam_role.api_gateway.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:DescribeLogGroups",
          "logs:DescribeLogStreams",
          "logs:PutLogEvents",
          "logs:GetLogEvents",
          "logs:FilterLogEvents"
        ]
        Resource = "*"
      }
    ]
  })
}

Variables Configuration

Create variables.tf:

variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "us-west-2"
}

variable "project_name" {
  description = "Name of the project"
  type        = string
}

variable "environment" {
  description = "Environment name"
  type        = string
  default     = "dev"
}

Output Configuration

Create outputs.tf:

output "api_endpoint" {
  description = "HTTP API Gateway endpoint"
  value       = aws_apigatewayv2_api.main.api_endpoint
}

output "lambda_function_name" {
  description = "Name of the Lambda function"
  value       = aws_lambda_function.hello_world.function_name
}

output "lambda_function_arn" {
  description = "ARN of the Lambda function"
  value       = aws_lambda_function.hello_world.arn
}

Best Practices

  1. Lambda Functions

    • Keep functions focused and small
    • Handle errors properly
    • Use environment variables
    • Implement proper logging
  2. API Gateway

    • Enable CORS when needed
    • Configure proper logging
    • Use appropriate authentication
    • Implement rate limiting
  3. Security

    • Use IAM roles with least privilege
    • Enable API Gateway authorization
    • Implement proper error handling
    • Monitor API usage

Deployment Steps

  1. Initialize Terraform:
terraform init
  1. Create terraform.tfvars:
aws_region   = "us-west-2"
project_name = "serverless-api"
environment  = "dev"
  1. Review the plan:
terraform plan
  1. Apply the configuration:
terraform apply

Testing the API

  1. Using curl:
# Get the API endpoint from Terraform output
API_ENDPOINT=$(terraform output -raw api_endpoint)

# Test the API
curl "${API_ENDPOINT}/hello?name=John"
  1. Using AWS Console:

    • Navigate to API Gateway
    • Select your API
    • Use the test feature
  2. Using Postman:

    • Create a new request
    • Use the API endpoint
    • Add query parameters

Monitoring and Troubleshooting

  1. CloudWatch Logs

    • Lambda function logs
    • API Gateway access logs
    • Error tracking
  2. Metrics

    • Lambda execution metrics
    • API Gateway metrics
    • Custom metrics
  3. X-Ray Tracing

    • Enable X-Ray tracing
    • Analyze request flow
    • Debug issues

Advanced Features

  1. Custom Authorizers
resource "aws_apigatewayv2_authorizer" "main" {
  api_id           = aws_apigatewayv2_api.main.id
  authorizer_type  = "JWT"
  identity_sources = ["$request.header.Authorization"]
  name            = "jwt-authorizer"
}
  1. Request Validation
resource "aws_apigatewayv2_model" "main" {
  api_id       = aws_apigatewayv2_api.main.id
  content_type = "application/json"
  name         = "validation-model"
  schema       = jsonencode({
    type = "object"
    required = ["name"]
    properties = {
      name = { type = "string" }
    }
  })
}
  1. Usage Plans
resource "aws_api_gateway_usage_plan" "main" {
  name = "${var.project_name}-usage-plan"

  api_stages {
    api_id = aws_apigatewayv2_api.main.id
    stage  = aws_apigatewayv2_stage.main.name
  }

  quota_settings {
    limit  = 1000
    period = "DAY"
  }

  throttle_settings {
    burst_limit = 5
    rate_limit  = 10
  }
}

Conclusion

You’ve learned how to create a serverless API using AWS Lambda and API Gateway with Terraform. This setup provides:

  • Scalable serverless architecture
  • Automated deployment
  • Proper monitoring and logging
  • Secure API endpoints

Remember to:

  • Follow security best practices
  • Monitor API usage
  • Implement proper error handling
  • Keep Lambda functions focused and efficient