Terraform #
In modern cloud architecture, Serverless and Infrastructure as Code (IaC) are two pillars that are almost inseparable. Serverless offers the advantage of freeing us from physical server management tasks, while IaC provides certainty that all those serverless resources are defined declaratively, consistently, reproducibly across environments, and recorded in a version control system (Git).
In the AWS environment, although Amazon provides native tools like AWS CloudFormation and the AWS Serverless Application Model (SAM), Terraform by HashiCorp remains the top choice for many enterprise organizations. Terraform offers multi-provider flexibility, the expressive HashiCorp Configuration Language (HCL) syntax, and a very extensive ecosystem. This article will deeply discuss how to operate Terraform to build and manage serverless architecture on AWS professionally.
Why Terraform Is Relevant for Serverless? #
There’s a common misconception that because serverless “has no servers,” we don’t need complex IaC tools. In reality, serverless architecture produces far more resources than traditional architecture.
A simple serverless API needs at least:
- One Lambda function.
- One IAM Execution Role with precise security policies.
- One log group in Amazon CloudWatch.
- Routes on Amazon API Gateway.
- Explicit permission for API Gateway to invoke that Lambda function.
If we create all these components manually through the AWS Web Console, our system becomes very vulnerable to human error, hard to document, and impossible to replicate identically to new environments (like staging or production). Terraform automates the entire creation of these resources and manages resource dependencies automatically.
flowchart TD
TF["Terraform Code (HCL)"] -->|terraform apply| Backend["S3 State Backend + DynamoDB Lock"]
TF -->|Creates| Role["AWS IAM Role (Execution Role)"]
TF -->|Creates| Lambda["AWS Lambda Function"]
TF -->|Creates| APIGW["Amazon API Gateway (HTTP API)"]
TF -->|Creates| Permission["AWS Lambda Permission (Allow API Gateway)"]
APIGW -->|Route Request| Lambda
Lambda -->|Runs under| RoleState Management at Production Scale #
Terraform uses a state file to record the mapping between real AWS resources and the HCL code we write. For team collaboration at production scale, we must configure a Remote State Backend that’s secure and supports state locking.
Remote State Configuration (S3 & DynamoDB Lock) #
We must store the state file in encrypted Amazon S3, and use Amazon DynamoDB to handle the locking mechanism (preventing two developers from running terraform apply simultaneously, which could corrupt the state file).
# CORRECT: Secure remote backend configuration at the project level
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "mycompany-terraform-states"
key = "serverless-app/prod/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true
dynamodb_table = "terraform-lock-table" // ✓ Used for state locking
}
}
Complete HCL Code Example: API Gateway & Lambda #
Here’s a functional, ready-to-use Terraform configuration example for deploying the API Gateway HTTP API -> AWS Lambda (Node.js) -> CloudWatch Logs architecture.
1. File: lambda.tf (Defining the Function and IAM Role)
#
# Automatically archiving JavaScript source code into a ZIP file
data "archive_file" "lambda_zip" {
type = "zip"
source_dir = "${path.module}/src"
output_path = "${path.module}/dist/lambda.zip"
}
# Creating the Execution Role for Lambda
resource "aws_iam_role" "lambda_exec" {
name = "app-lambda-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}]
})
}
# Attaching the standard policy for writing logs to CloudWatch
resource "aws_iam_role_policy_attachment" "lambda_logs" {
role = aws_iam_role.lambda_exec.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
# Creating the Lambda Function
resource "aws_lambda_function" "api_handler" {
filename = data.archive_file.lambda_zip.output_path
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
function_name = "app-api-handler"
role = aws_iam_role.lambda_exec.arn
handler = "index.handler"
runtime = "nodejs20.x"
timeout = 10
memory_size = 256
environment {
variables = {
NODE_ENV = "production"
}
}
# Ensuring the log group is created before the function deploys
depends_on = [
aws_iam_role_policy_attachment.lambda_logs,
aws_cloudwatch_log_group.lambda_log_group
]
}
# Explicitly creating a CloudWatch Log Group to manage log retention
resource "aws_cloudwatch_log_group" "lambda_log_group" {
name = "/aws/lambda/app-api-handler"
retention_in_days = 7 // ✓ Preventing ballooning bills from unlimited log storage
}
2. File: api_gateway.tf (Defining the Gateway and Permissions)
#
# Creating an API Gateway HTTP API (Cheaper and faster than REST API)
resource "aws_apigatewayv2_api" "http_api" {
name = "app-http-api"
protocol_type = "HTTP"
}
# Creating a Stage for deployment (dev, prod, etc.)
resource "aws_apigatewayv2_stage" "prod" {
api_id = aws_apigatewayv2_api.http_api.id
name = "prod"
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"
})
}
}
# Creating a CloudWatch Log Group for API Gateway Access Logs
resource "aws_cloudwatch_log_group" "api_gw" {
name = "/aws/api-gw/app-http-api"
retention_in_days = 7
}
# Configuring the integration between API Gateway and the Lambda function
resource "aws_apigatewayv2_integration" "lambda" {
api_id = aws_apigatewayv2_api.http_api.id
integration_type = "AWS_PROXY" // ✓ Lambda Proxy Integration
integration_method = "POST"
integration_uri = aws_lambda_function.api_handler.invoke_arn
payload_format_version = "2.0"
}
# Creating the default route (Catching all HTTP requests)
resource "aws_apigatewayv2_route" "any" {
api_id = aws_apigatewayv2_api.http_api.id
route_key = "ANY /{proxy+}"
target = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}
# ✓ IMPORTANT: Giving API Gateway explicit permission to invoke Lambda
resource "aws_lambda_permission" "api_gw" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.api_handler.function_name
principal = "apigateway.amazonaws.com"
# Restricting invocation permission only to our specific API Gateway
source_arn = "${aws_apigatewayv2_api.http_api.execution_arn}/*/*"
}
# Output for printing the API Gateway URL after terraform apply completes
output "api_url" {
value = "${aws_apigatewayv2_stage.prod.invoke_url}/"
description = "Public HTTP API endpoint URL"
}
Comparison: Terraform vs. AWS SAM / Serverless Framework #
Many teams are often confused choosing between Terraform and other serverless-specific tools.
| Evaluation Dimension | Terraform | AWS SAM / Serverless Framework |
|---|---|---|
| Management Scope | Entire cloud infrastructure (VPC, RDS, IAM, S3, etc.) | Primary focus on serverless application resources |
| Code Iteration Speed | Medium (ZIP compression & upload managed manually/HCL) | Very fast (has instant code deploy commands) |
| Syntax Abstraction | Low (writes all components explicitly) | High (a few lines of YAML generate many resources) |
| State Management | Must be managed yourself (S3 backend) | Automatically managed by CloudFormation behind the scenes |
Hybrid Approach Recommendation #
For large-scale projects, the best approach is a hybrid approach:
- Use Terraform to build the semi-permanent, rarely-changing foundation infrastructure (like VPC, Amazon RDS Database, S3 Buckets, Amazon SQS Queues).
- Use the Serverless Framework or AWS SAM to deploy frequently-changing top-level applications (like Lambda functions and API Gateway routes). We can reference foundation infrastructure ARNs from Terraform using the SSM Parameter Store data source.
Summary #
← Previous: API Gateway Next: Cloud Functions →
- Terraform simplifies serverless management by defining complex infrastructure declaratively and consistently (IaC).
- Always use a Remote State Backend based on Amazon S3 with encryption enabled, plus DynamoDB to handle the state locking mechanism securely.
- Configure CloudWatch Log Group retention explicitly in Terraform to prevent cost ballooning from unlimited log storage.
- Give explicit permission via
aws_lambda_permissionso API Gateway has the access rights to trigger Lambda function execution.- Apply the hybrid approach: use Terraform for network/database foundations, and use SAM/Serverless Framework for dynamic daily Lambda code deployment.