DevOps Zero to Hero: Part 5 - Infrastructure as Code with Terraform
Introduction
Infrastructure as Code (IaC) revolutionizes how we provision and manage infrastructure. Instead of manual clicking through cloud consoles, we define our infrastructure in code files that can be versioned, reviewed, and automatically deployed. Terraform is the industry-leading tool for IaC, supporting multiple cloud providers with a consistent workflow.
What is Infrastructure as Code?
Traditional vs IaC Approach
Traditional Infrastructure Management:
Manual provisioning through GUI
Inconsistent environments
No version control
Difficult to replicate
Prone to configuration drift
Time-consuming and error-prone
Infrastructure as Code:
Declarative configuration files
Version controlled
Automated provisioning
Consistent and repeatable
Self-documenting
Enables GitOps workflows
Terraform Fundamentals
Why Terraform?
Cloud Agnostic: Works with AWS, Azure, GCP, and 100+ providers
Declarative Syntax: Describe desired state, not steps
State Management: Tracks real-world resources
Plan Before Apply: Preview changes before execution
Modular: Reusable components via modules
Large Ecosystem: Extensive provider and module registry
Core Concepts
Providers: Plugins that interact with cloud platforms
Resources: Infrastructure components (EC2, S3, etc.)
Variables: Input parameters for configurations
Outputs: Return values from configurations
State: Record of managed infrastructure
Modules: Reusable Terraform configurations
Installing Terraform
Installation Methods
# macOS with Homebrew
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
# Linux
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
# Windows with Chocolatey
choco install terraform
# Verify installation
terraform --version
AWS CLI Setup
# Install AWS CLI
# macOS
brew install awscli
# Linux
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
# Configure AWS credentials
aws configure
# Enter your AWS Access Key ID
# Enter your AWS Secret Access Key
# Enter default region (e.g., us-east-1)
# Enter default output format (json)
Your First Terraform Configuration
Project Structure
terraform-infrastructure/
├── main.tf # Main configuration
├── variables.tf # Variable definitions
├── outputs.tf # Output definitions
├── terraform.tfvars # Variable values
├── versions.tf # Provider versions
└── modules/ # Custom modules
├── networking/
├── compute/
└── storage/
Basic Configuration
Create versions.tf
:
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "Terraform"
}
}
}
Create variables.tf
:
variable "aws_region" {
description = "AWS region for resources"
type = string
default = "us-east-1"
}
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "project_name" {
description = "Project name"
type = string
default = "devops-web-app"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "availability_zones" {
description = "Availability zones"
type = list(string)
default = ["us-east-1a", "us-east-1b"]
}
variable "tags" {
description = "Additional tags"
type = map(string)
default = {}
}
Create main.tf
:
# Data source for latest Amazon Linux 2 AMI
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-vpc-${var.environment}"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-igw-${var.environment}"
}
}
# Public Subnets
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-subnet-${count.index + 1}-${var.environment}"
Type = "Public"
}
}
# Private Subnets
resource "aws_subnet" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 10}.0/24"
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.project_name}-private-subnet-${count.index + 1}-${var.environment}"
Type = "Private"
}
}
# Route Table for Public Subnets
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.project_name}-public-rt-${var.environment}"
}
}
# Route Table Associations
resource "aws_route_table_association" "public" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# Security Group
resource "aws_security_group" "web" {
name = "${var.project_name}-web-sg-${var.environment}"
description = "Security group for web servers"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP from anywhere"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS from anywhere"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "SSH from anywhere"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "Allow all outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-web-sg-${var.environment}"
}
}
# EC2 Instance
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
subnet_id = aws_subnet.public[0].id
vpc_security_group_ids = [aws_security_group.web.id]
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y docker
service docker start
usermod -a -G docker ec2-user
docker run -d -p 80:3000 --name web-app ${var.project_name}:latest
EOF
tags = {
Name = "${var.project_name}-web-${var.environment}"
}
}
# S3 Bucket for Application Assets
resource "aws_s3_bucket" "assets" {
bucket = "${var.project_name}-assets-${var.environment}-${random_id.bucket_suffix.hex}"
tags = {
Name = "${var.project_name}-assets-${var.environment}"
}
}
resource "aws_s3_bucket_versioning" "assets" {
bucket = aws_s3_bucket.assets.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_public_access_block" "assets" {
bucket = aws_s3_bucket.assets.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "random_id" "bucket_suffix" {
byte_length = 4
}
Create outputs.tf
:
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "IDs of public subnets"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "IDs of private subnets"
value = aws_subnet.private[*].id
}
output "web_instance_public_ip" {
description = "Public IP of web instance"
value = aws_instance.web.public_ip
}
output "web_instance_dns" {
description = "Public DNS of web instance"
value = aws_instance.web.public_dns
}
output "s3_bucket_name" {
description = "Name of S3 bucket"
value = aws_s3_bucket.assets.id
}
output "security_group_id" {
description = "ID of web security group"
value = aws_security_group.web.id
}
Terraform Commands and Workflow
Essential Commands
# Initialize Terraform
terraform init
# Format code
terraform fmt -recursive
# Validate configuration
terraform validate
# Plan changes
terraform plan
# Apply changes
terraform apply
# Apply with auto-approve (use carefully)
terraform apply -auto-approve
# Show current state
terraform show
# List resources
terraform state list
# Destroy infrastructure
terraform destroy
# Get outputs
terraform output
## Advanced Terraform Concepts
### Terraform Modules
Modules promote reusability and organization. Create `modules/vpc/main.tf`:
```hcl
# modules/vpc/main.tf
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
}
variable "environment" {
description = "Environment name"
type = string
}
variable "project_name" {
description = "Project name"
type = string
}
variable "availability_zones" {
description = "List of availability zones"
type = list(string)
}
locals {
public_subnet_cidrs = [for i in range(length(var.availability_zones)) : cidrsubnet(var.vpc_cidr, 8, i)]
private_subnet_cidrs = [for i in range(length(var.availability_zones)) : cidrsubnet(var.vpc_cidr, 8, i + 10)]
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-vpc-${var.environment}"
Environment = var.environment
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-igw-${var.environment}"
Environment = var.environment
}
}
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = local.public_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-${var.availability_zones[count.index]}-${var.environment}"
Environment = var.environment
Type = "Public"
}
}
resource "aws_subnet" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = local.private_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.project_name}-private-${var.availability_zones[count.index]}-${var.environment}"
Environment = var.environment
Type = "Private"
}
}
resource "aws_eip" "nat" {
count = length(var.availability_zones)
domain = "vpc"
tags = {
Name = "${var.project_name}-nat-eip-${count.index + 1}-${var.environment}"
Environment = var.environment
}
}
resource "aws_nat_gateway" "main" {
count = length(var.availability_zones)
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "${var.project_name}-nat-${count.index + 1}-${var.environment}"
Environment = var.environment
}
depends_on = [aws_internet_gateway.main]
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.project_name}-public-rt-${var.environment}"
Environment = var.environment
}
}
resource "aws_route_table" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[count.index].id
}
tags = {
Name = "${var.project_name}-private-rt-${count.index + 1}-${var.environment}"
Environment = var.environment
}
}
resource "aws_route_table_association" "public" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "private" {
count = length(aws_subnet.private)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
output "vpc_id" {
value = aws_vpc.main.id
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
Using the module in main configuration:
module "vpc" {
source = "./modules/vpc"
vpc_cidr = "10.0.0.0/16"
environment = var.environment
project_name = var.project_name
availability_zones = var.availability_zones
}
# Reference module outputs
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
subnet_id = module.vpc.public_subnet_ids[0]
# ... rest of configuration
}
State Management
Remote State with S3
Create backend.tf
:
terraform {
backend "s3" {
bucket = "terraform-state-bucket-unique-name"
key = "devops-web-app/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
Setup S3 backend:
# Create S3 bucket for state
aws s3api create-bucket --bucket terraform-state-bucket-unique-name --region us-east-1
# Enable versioning
aws s3api put-bucket-versioning --bucket terraform-state-bucket-unique-name --versioning-configuration Status=Enabled
# Create DynamoDB table for state locking
aws dynamodb create-table \
--table-name terraform-state-lock \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5
State Commands
# List resources in state
terraform state list
# Show specific resource
terraform state show aws_instance.web
# Move resource
terraform state mv aws_instance.web aws_instance.app
# Remove from state (doesn't destroy actual resource)
terraform state rm aws_instance.web
# Import existing resource
terraform import aws_instance.web i-1234567890abcdef0
# Pull remote state
terraform state pull > terraform.tfstate
# Push local state to remote
terraform state push terraform.tfstate
Workspaces
Workspaces allow multiple states from same configuration:
# List workspaces
terraform workspace list
# Create new workspace
terraform workspace new staging
# Select workspace
terraform workspace select staging
# Show current workspace
terraform workspace show
# Delete workspace
terraform workspace delete staging
Use workspace in configuration:
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = terraform.workspace == "prod" ? "t3.large" : "t3.micro"
tags = {
Name = "${var.project_name}-web-${terraform.workspace}"
Environment = terraform.workspace
}
}
Real-World Infrastructure
Complete ECS Infrastructure
Create ecs-infrastructure.tf
:
# ECS Cluster
resource "aws_ecs_cluster" "main" {
name = "${var.project_name}-cluster-${var.environment}"
setting {
name = "containerInsights"
value = "enabled"
}
tags = {
Name = "${var.project_name}-cluster-${var.environment}"
Environment = var.environment
}
}
# ECS Task Definition
resource "aws_ecs_task_definition" "app" {
family = "${var.project_name}-task-${var.environment}"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "256"
memory = "512"
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn
container_definitions = jsonencode([
{
name = var.project_name
image = "${aws_ecr_repository.app.repository_url}:latest"
portMappings = [
{
containerPort = 3000
protocol = "tcp"
}
]
environment = [
{
name = "NODE_ENV"
value = var.environment
},
{
name = "PORT"
value = "3000"
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.ecs.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "ecs"
}
}
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
}
])
tags = {
Name = "${var.project_name}-task-${var.environment}"
Environment = var.environment
}
}
# Application Load Balancer
resource "aws_lb" "main" {
name = "${var.project_name}-alb-${var.environment}"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = module.vpc.public_subnet_ids
enable_deletion_protection = var.environment == "prod" ? true : false
enable_http2 = true
enable_cross_zone_load_balancing = true
tags = {
Name = "${var.project_name}-alb-${var.environment}"
Environment = var.environment
}
}
# Target Group
resource "aws_lb_target_group" "app" {
name = "${var.project_name}-tg-${var.environment}"
port = 3000
protocol = "HTTP"
vpc_id = module.vpc.vpc_id
target_type = "ip"
health_check {
enabled = true
healthy_threshold = 2
unhealthy_threshold = 2
timeout = 5
interval = 30
path = "/health"
matcher = "200"
}
deregistration_delay = 30
tags = {
Name = "${var.project_name}-tg-${var.environment}"
Environment = var.environment
}
}
# ALB Listener
resource "aws_lb_listener" "app" {
load_balancer_arn = aws_lb.main.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
# ECS Service
resource "aws_ecs_service" "app" {
name = "${var.project_name}-service-${var.environment}"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = var.app_count
launch_type = "FARGATE"
network_configuration {
subnets = module.vpc.private_subnet_ids
security_groups = [aws_security_group.ecs_tasks.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.app.arn
container_name = var.project_name
container_port = 3000
}
deployment_configuration {
maximum_percent = 200
minimum_healthy_percent = 100
}
deployment_circuit_breaker {
enable = true
rollback = true
}
depends_on = [aws_lb_listener.app]
tags = {
Name = "${var.project_name}-service-${var.environment}"
Environment = var.environment
}
}
# Auto Scaling
resource "aws_appautoscaling_target" "ecs" {
max_capacity = 10
min_capacity = 2
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.app.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "cpu" {
name = "${var.project_name}-cpu-scaling-${var.environment}"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.ecs.resource_id
scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
service_namespace = aws_appautoscaling_target.ecs.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = 70.0
}
}
# CloudWatch Log Group
resource "aws_cloudwatch_log_group" "ecs" {
name = "/ecs/${var.project_name}-${var.environment}"
retention_in_days = var.environment == "prod" ? 30 : 7
tags = {
Name = "${var.project_name}-logs-${var.environment}"
Environment = var.environment
}
}
# ECR Repository
resource "aws_ecr_repository" "app" {
name = "${var.project_name}-${var.environment}"
image_tag_mutability = "MUTABLE"
image_scanning_configuration {
scan_on_push = true
}
encryption_configuration {
encryption_type = "AES256"
}
tags = {
Name = "${var.project_name}-ecr-${var.environment}"
Environment = var.environment
}
}
resource "aws_ecr_lifecycle_policy" "app" {
repository = aws_ecr_repository.app.name
policy = jsonencode({
rules = [
{
rulePriority = 1
description = "Keep last 10 images"
selection = {
tagStatus = "tagged"
tagPrefixList = ["v"]
countType = "imageCountMoreThan"
countNumber = 10
}
action = {
type = "expire"
}
},
{
rulePriority = 2
description = "Remove untagged images after 7 days"
selection = {
tagStatus = "untagged"
countType = "sinceImagePushed"
countUnit = "days"
countNumber = 7
}
action = {
type = "expire"
}
}
]
})
}
Lambda and EventBridge Infrastructure
Create serverless-infrastructure.tf
:
# Lambda Function
resource "aws_lambda_function" "processor" {
filename = "lambda_function.zip"
function_name = "${var.project_name}-processor-${var.environment}"
role = aws_iam_role.lambda.arn
handler = "index.handler"
source_code_hash = filebase64sha256("lambda_function.zip")
runtime = "nodejs18.x"
timeout = 30
memory_size = 256
environment {
variables = {
ENVIRONMENT = var.environment
TABLE_NAME = aws_dynamodb_table.events.name
}
}
vpc_config {
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [aws_security_group.lambda.id]
}
dead_letter_config {
target_arn = aws_sqs_queue.dlq.arn
}
tracing_config {
mode = "Active"
}
tags = {
Name = "${var.project_name}-processor-${var.environment}"
Environment = var.environment
}
}
# EventBridge Rule
resource "aws_cloudwatch_event_rule" "schedule" {
name = "${var.project_name}-schedule-${var.environment}"
description = "Trigger Lambda function on schedule"
schedule_expression = "rate(5 minutes)"
tags = {
Name = "${var.project_name}-schedule-${var.environment}"
Environment = var.environment
}
}
resource "aws_cloudwatch_event_target" "lambda" {
rule = aws_cloudwatch_event_rule.schedule.name
target_id = "LambdaTarget"
arn = aws_lambda_function.processor.arn
}
resource "aws_lambda_permission" "eventbridge" {
statement_id = "AllowExecutionFromEventBridge"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.processor.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.schedule.arn
}
# Custom EventBridge Event Bus
resource "aws_cloudwatch_event_bus" "custom" {
name = "${var.project_name}-events-${var.environment}"
tags = {
Name = "${var.project_name}-events-${var.environment}"
Environment = var.environment
}
}
# Event Rule for Custom Events
resource "aws_cloudwatch_event_rule" "custom_events" {
name = "${var.project_name}-custom-events-${var.environment}"
description = "Capture custom application events"
event_bus_name = aws_cloudwatch_event_bus.custom.name
event_pattern = jsonencode({
source = ["custom.application"]
detail-type = ["Order Placed", "User Registered"]
})
tags = {
Name = "${var.project_name}-custom-events-${var.environment}"
Environment = var.environment
}
}
# DynamoDB Table for Events
resource "aws_dynamodb_table" "events" {
name = "${var.project_name}-events-${var.environment}"
billing_mode = "PAY_PER_REQUEST"
hash_key = "event_id"
range_key = "timestamp"
attribute {
name = "event_id"
type = "S"
}
attribute {
name = "timestamp"
type = "N"
}
attribute {
name = "event_type"
type = "S"
}
global_secondary_index {
name = "EventTypeIndex"
hash_key = "event_type"
range_key = "timestamp"
projection_type = "ALL"
}
ttl {
attribute_name = "ttl"
enabled = true
}
point_in_time_recovery {
enabled = var.environment == "prod" ? true : false
}
server_side_encryption {
enabled = true
}
tags = {
Name = "${var.project_name}-events-${var.environment}"
Environment = var.environment
}
}
# SQS Dead Letter Queue
resource "aws_sqs_queue" "dlq" {
name = "${var.project_name}-dlq-${var.environment}"
delay_seconds = 0
max_message_size = 262144
message_retention_seconds = 1209600 # 14 days
receive_wait_time_seconds = 10
tags = {
Name = "${var.project_name}-dlq-${var.environment}"
Environment = var.environment
}
}
# API Gateway for Lambda
resource "aws_api_gateway_rest_api" "api" {
name = "${var.project_name}-api-${var.environment}"
description = "API Gateway for Lambda functions"
endpoint_configuration {
types = ["REGIONAL"]
}
tags = {
Name = "${var.project_name}-api-${var.environment}"
Environment = var.environment
}
}
resource "aws_api_gateway_resource" "proxy" {
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_rest_api.api.root_resource_id
path_part = "{proxy+}"
}
resource "aws_api_gateway_method" "proxy" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.proxy.id
http_method = "ANY"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "lambda" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_method.proxy.resource_id
http_method = aws_api_gateway_method.proxy.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.processor.invoke_arn
}
resource "aws_api_gateway_deployment" "api" {
depends_on = [
aws_api_gateway_integration.lambda
]
rest_api_id = aws_api_gateway_rest_api.api.id
stage_name = var.environment
}
Terraform Best Practices
1. File Organization
terraform/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ └── prod/
├── modules/
│ ├── vpc/
│ ├── ecs/
│ ├── rds/
│ └── lambda/
└── global/
├── iam/
└── s3/
2. Variable Validation
variable "instance_type" {
description = "EC2 instance type"
type = string
validation {
condition = can(regex("^t3\\.", var.instance_type))
error_message = "Instance type must be from t3 family."
}
}
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
3. Dynamic Blocks
resource "aws_security_group" "dynamic" {
name = "dynamic-sg"
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
4. Conditional Resources
resource "aws_instance" "web" {
count = var.create_instance ? 1 : 0
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
}
resource "aws_eip" "web" {
count = var.create_instance && var.assign_eip ? 1 : 0
instance = aws_instance.web[0].id
domain = "vpc"
}
5. Data Sources
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
data "aws_availability_zones" "available" {
state = "available"
}
locals {
account_id = data.aws_caller_identity.current.account_id
region = data.aws_region.current.name
azs = data.aws_availability_zones.available.names
}
Terraform CI/CD Integration
GitHub Actions Workflow
Create .github/workflows/terraform.yml
:
name: Terraform CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
TF_VERSION: "1.5.0"
TF_VAR_environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }}
jobs:
terraform:
name: Terraform Plan and Apply
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Terraform Init
run: terraform init
- name: Terraform Format Check
run: terraform fmt -check -recursive
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
id: plan
run: terraform plan -out=tfplan
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve tfplan
Troubleshooting Terraform
Common Issues
State Lock Error
# Force unlock (use carefully)
terraform force-unlock <lock-id>
Resource Already Exists
# Import existing resource
terraform import aws_instance.web i-1234567890abcdef0
State Drift
# Refresh state
terraform refresh
# Or use -refresh-only mode
terraform apply -refresh-only
Dependency Errors
# Use depends_on for explicit dependencies
resource "aws_instance" "web" {
# ...
depends_on = [aws_security_group.web]
}
Key Takeaways
Infrastructure as Code enables version control, automation, and consistency
Terraform provides a declarative way to manage infrastructure across multiple providers
State management is crucial for tracking real-world resources
Modules promote reusability and maintainability
Remote state enables team collaboration
Always plan before applying changes
Use workspaces or separate directories for different environments
What's Next?
In Part 6, we'll explore AWS Fundamentals for DevOps. You'll learn:
AWS core services overview
IAM and security best practices
Networking in AWS
Compute services (EC2, ECS, Lambda)
Storage solutions (S3, EFS, EBS)
Database services (RDS, DynamoDB)
Monitoring with CloudWatch
Additional Resources
Continue your journey with Part 6: AWS Fundamentals for DevOps!