Terraform is an Infrastructure as Code (IaC) tool developed by HashiCorp. It allows you to manage cloud provider resources like AWS with declarative code. This article covers practical AWS infrastructure building from basics to production-level operations.
What You’ll Learn
- Terraform basic concepts and HCL syntax
- Building AWS resources (VPC, EC2, RDS, S3)
- Module design and best practices
- State management and team development
- CI/CD pipeline integration
Environment Setup
Installation
# macOS (Homebrew)
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
# Linux
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /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
# Version check
terraform version
AWS Authentication Setup
# AWS CLI configuration
aws configure
# AWS Access Key ID: AKIAIOSFODNN7EXAMPLE
# AWS Secret Access Key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Default region name: ap-northeast-1
# Or environment variables
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_DEFAULT_REGION="ap-northeast-1"
Project Structure
terraform-project/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ └── prod/
├── modules/
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── compute/
│ ├── database/
│ └── storage/
├── .terraform-version
└── .gitignore
HCL Basic Syntax
Provider Configuration
# main.tf
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# Remote backend (explained later)
backend "s3" {
bucket = "my-terraform-state"
key = "dev/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "Terraform"
}
}
}
Variable Definitions
# variables.tf
variable "aws_region" {
description = "AWS region"
type = string
default = "ap-northeast-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
}
variable "vpc_cidr" {
description = "VPC CIDR block"
type = string
default = "10.0.0.0/16"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "db_config" {
description = "Database configuration"
type = object({
instance_class = string
allocated_storage = number
engine_version = string
})
default = {
instance_class = "db.t3.micro"
allocated_storage = 20
engine_version = "16.1"
}
}
Variable Values File
# terraform.tfvars
aws_region = "ap-northeast-1"
environment = "dev"
project_name = "my-app"
vpc_cidr = "10.0.0.0/16"
instance_type = "t3.small"
db_config = {
instance_class = "db.t3.small"
allocated_storage = 50
engine_version = "16.1"
}
VPC Network Construction
Module Design
# modules/networking/main.tf
locals {
azs = slice(data.aws_availability_zones.available.names, 0, 3)
}
data "aws_availability_zones" "available" {
state = "available"
}
# VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-${var.environment}-vpc"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-${var.environment}-igw"
}
}
# Public Subnets
resource "aws_subnet" "public" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index)
availability_zone = local.azs[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-${var.environment}-public-${local.azs[count.index]}"
Tier = "Public"
}
}
# Private Subnets
resource "aws_subnet" "private" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index + length(local.azs))
availability_zone = local.azs[count.index]
tags = {
Name = "${var.project_name}-${var.environment}-private-${local.azs[count.index]}"
Tier = "Private"
}
}
# NAT Gateway (for production environments)
resource "aws_eip" "nat" {
count = var.enable_nat_gateway ? length(local.azs) : 0
domain = "vpc"
tags = {
Name = "${var.project_name}-${var.environment}-nat-eip-${count.index}"
}
}
resource "aws_nat_gateway" "main" {
count = var.enable_nat_gateway ? length(local.azs) : 0
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "${var.project_name}-${var.environment}-nat-${count.index}"
}
depends_on = [aws_internet_gateway.main]
}
# Route Table (Public)
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}-${var.environment}-public-rt"
}
}
resource "aws_route_table_association" "public" {
count = length(local.azs)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# Route Table (Private)
resource "aws_route_table" "private" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
dynamic "route" {
for_each = var.enable_nat_gateway ? [1] : []
content {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[count.index].id
}
}
tags = {
Name = "${var.project_name}-${var.environment}-private-rt-${count.index}"
}
}
resource "aws_route_table_association" "private" {
count = length(local.azs)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
Module Variables and Outputs
# modules/networking/variables.tf
variable "project_name" {
type = string
}
variable "environment" {
type = string
}
variable "vpc_cidr" {
type = string
default = "10.0.0.0/16"
}
variable "enable_nat_gateway" {
type = bool
default = false
}
# modules/networking/outputs.tf
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "Public subnet IDs"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "Private subnet IDs"
value = aws_subnet.private[*].id
}
EC2 Instance Construction
Security Groups
# modules/compute/main.tf
resource "aws_security_group" "web" {
name = "${var.project_name}-${var.environment}-web-sg"
description = "Security group for web servers"
vpc_id = var.vpc_id
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-${var.environment}-web-sg"
}
}
Launch Template and Auto Scaling
# AMI Data Source
data "aws_ami" "amazon_linux_2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# Launch Template
resource "aws_launch_template" "web" {
name_prefix = "${var.project_name}-${var.environment}-web-"
image_id = data.aws_ami.amazon_linux_2023.id
instance_type = var.instance_type
network_interfaces {
associate_public_ip_address = false
security_groups = [aws_security_group.web.id]
}
iam_instance_profile {
name = aws_iam_instance_profile.web.name
}
user_data = base64encode(templatefile("${path.module}/templates/user_data.sh", {
environment = var.environment
region = var.aws_region
}))
monitoring {
enabled = true
}
tag_specifications {
resource_type = "instance"
tags = {
Name = "${var.project_name}-${var.environment}-web"
}
}
lifecycle {
create_before_destroy = true
}
}
# Auto Scaling Group
resource "aws_autoscaling_group" "web" {
name = "${var.project_name}-${var.environment}-web-asg"
vpc_zone_identifier = var.private_subnet_ids
target_group_arns = [aws_lb_target_group.web.arn]
health_check_type = "ELB"
min_size = var.asg_min_size
max_size = var.asg_max_size
desired_capacity = var.asg_desired_capacity
launch_template {
id = aws_launch_template.web.id
version = "$Latest"
}
instance_refresh {
strategy = "Rolling"
preferences {
min_healthy_percentage = 50
}
}
tag {
key = "Name"
value = "${var.project_name}-${var.environment}-web"
propagate_at_launch = true
}
}
# Scaling Policies
resource "aws_autoscaling_policy" "web_scale_up" {
name = "${var.project_name}-${var.environment}-web-scale-up"
autoscaling_group_name = aws_autoscaling_group.web.name
adjustment_type = "ChangeInCapacity"
scaling_adjustment = 1
cooldown = 300
}
resource "aws_autoscaling_policy" "web_scale_down" {
name = "${var.project_name}-${var.environment}-web-scale-down"
autoscaling_group_name = aws_autoscaling_group.web.name
adjustment_type = "ChangeInCapacity"
scaling_adjustment = -1
cooldown = 300
}
RDS Database Construction
# modules/database/main.tf
# Subnet Group
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-${var.environment}-db-subnet"
subnet_ids = var.private_subnet_ids
tags = {
Name = "${var.project_name}-${var.environment}-db-subnet"
}
}
# Security Group
resource "aws_security_group" "db" {
name = "${var.project_name}-${var.environment}-db-sg"
description = "Security group for RDS"
vpc_id = var.vpc_id
ingress {
description = "PostgreSQL from web servers"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [var.web_security_group_id]
}
tags = {
Name = "${var.project_name}-${var.environment}-db-sg"
}
}
# Parameter Group
resource "aws_db_parameter_group" "main" {
family = "postgres16"
name = "${var.project_name}-${var.environment}-pg16"
parameter {
name = "log_min_duration_statement"
value = "1000" # Log queries over 1 second
}
parameter {
name = "shared_preload_libraries"
value = "pg_stat_statements"
}
}
# RDS Instance
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-${var.environment}-db"
engine = "postgres"
engine_version = var.db_config.engine_version
instance_class = var.db_config.instance_class
allocated_storage = var.db_config.allocated_storage
max_allocated_storage = var.db_config.allocated_storage * 2
storage_type = "gp3"
storage_encrypted = true
db_name = var.db_name
username = var.db_username
password = var.db_password # Secrets Manager recommended
multi_az = var.environment == "prod"
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.db.id]
parameter_group_name = aws_db_parameter_group.main.name
backup_retention_period = var.environment == "prod" ? 7 : 1
backup_window = "03:00-04:00"
maintenance_window = "mon:04:00-mon:05:00"
skip_final_snapshot = var.environment != "prod"
final_snapshot_identifier = var.environment == "prod" ? "${var.project_name}-${var.environment}-final" : null
deletion_protection = var.environment == "prod"
performance_insights_enabled = true
monitoring_interval = 60
monitoring_role_arn = aws_iam_role.rds_monitoring.arn
tags = {
Name = "${var.project_name}-${var.environment}-db"
}
}
State Management
Remote Backend Setup
# backend-setup/main.tf
# Create S3 bucket and DynamoDB table for state management
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-terraform-state-${data.aws_caller_identity.current.account_id}"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
resource "aws_s3_bucket_public_access_block" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
CI/CD Integration
GitHub Actions
# .github/workflows/terraform.yml
name: Terraform
on:
push:
branches: [main]
paths:
- 'terraform/**'
pull_request:
branches: [main]
paths:
- 'terraform/**'
env:
TF_VERSION: '1.6.0'
AWS_REGION: 'ap-northeast-1'
jobs:
terraform:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions
aws-region: ${{ env.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Format
id: fmt
run: terraform fmt -check -recursive
continue-on-error: true
- name: Terraform Init
id: init
run: terraform init
working-directory: terraform/environments/dev
- name: Terraform Validate
id: validate
run: terraform validate -no-color
- name: Terraform Plan
id: plan
if: github.event_name == 'pull_request'
run: terraform plan -no-color -input=false
working-directory: terraform/environments/dev
continue-on-error: true
- name: Update Pull Request
uses: actions/github-script@v7
if: github.event_name == 'pull_request'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `#### Terraform Format: \`${{ steps.fmt.outcome }}\`
#### Terraform Init: \`${{ steps.init.outcome }}\`
#### Terraform Validate: \`${{ steps.validate.outcome }}\`
#### Terraform Plan: \`${{ steps.plan.outcome }}\`
<details><summary>Show Plan</summary>
\`\`\`terraform
${{ steps.plan.outputs.stdout }}
\`\`\`
</details>`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve -input=false
working-directory: terraform/environments/dev
Best Practices
Naming Conventions
# Consistent naming convention
locals {
name_prefix = "${var.project_name}-${var.environment}"
}
resource "aws_vpc" "main" {
tags = {
Name = "${local.name_prefix}-vpc"
}
}
resource "aws_subnet" "public" {
tags = {
Name = "${local.name_prefix}-public-subnet-${count.index + 1}"
}
}
Utilizing Data Sources
# Reference existing resources
data "aws_vpc" "existing" {
filter {
name = "tag:Name"
values = ["existing-vpc"]
}
}
data "aws_subnets" "private" {
filter {
name = "vpc-id"
values = [data.aws_vpc.existing.id]
}
filter {
name = "tag:Tier"
values = ["Private"]
}
}
Conditional Logic
# Conditional by environment
resource "aws_nat_gateway" "main" {
count = var.environment == "prod" ? length(local.azs) : 1
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
}
# Dynamic blocks
resource "aws_security_group" "example" {
dynamic "ingress" {
for_each = var.allowed_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}
Command Reference
# Initialize
terraform init
# Format
terraform fmt -recursive
# Validate
terraform validate
# Plan
terraform plan -out=plan.tfplan
# Apply
terraform apply plan.tfplan
# Destroy
terraform destroy
# State inspection
terraform state list
terraform state show aws_vpc.main
# Import
terraform import aws_vpc.main vpc-12345678
# Output
terraform output
terraform output -json
Summary
Here are the key points for infrastructure building with Terraform.
Design Principles
- Modularization: Reusable component design
- Environment Separation: Independent state management per environment
- Naming Conventions: Consistent naming
- Documentation: Enrich variable descriptions
Operational Points
- Remote Backend: State management with S3 + DynamoDB
- CI/CD Integration: Automation with GitHub Actions
- Plan Review: Always verify before applying
- Version Management: Pin provider and Terraform versions
Infrastructure as Code enables reproducibility, traceability, and automation.