Infrastructure as Code (IaC) treats infrastructure like software: versioned, tested, and repeatable. Terraform is the leading tool for declarative infrastructure management across cloud providers.
Core Concepts#
Declarative vs Imperative#
1# Declarative: Describe desired state
2# Terraform figures out how to get there
3
4resource "aws_instance" "web" {
5 ami = "ami-0c55b159cbfafe1f0"
6 instance_type = "t3.micro"
7
8 tags = {
9 Name = "web-server"
10 }
11}
12
13# Terraform will:
14# - Create if doesn't exist
15# - Update if configuration changed
16# - Do nothing if already matchesProviders#
1# Configure providers
2terraform {
3 required_providers {
4 aws = {
5 source = "hashicorp/aws"
6 version = "~> 5.0"
7 }
8 cloudflare = {
9 source = "cloudflare/cloudflare"
10 version = "~> 4.0"
11 }
12 }
13}
14
15provider "aws" {
16 region = "us-east-1"
17}
18
19provider "cloudflare" {
20 api_token = var.cloudflare_api_token
21}Resources and Data Sources#
1# Resource: Managed by Terraform
2resource "aws_s3_bucket" "data" {
3 bucket = "my-data-bucket"
4}
5
6# Data source: Read-only, external data
7data "aws_ami" "ubuntu" {
8 most_recent = true
9 owners = ["099720109477"]
10
11 filter {
12 name = "name"
13 values = ["ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"]
14 }
15}
16
17resource "aws_instance" "web" {
18 ami = data.aws_ami.ubuntu.id
19 instance_type = "t3.micro"
20}Variables and Outputs#
Input Variables#
1# variables.tf
2variable "environment" {
3 description = "Deployment environment"
4 type = string
5 default = "development"
6
7 validation {
8 condition = contains(["development", "staging", "production"], var.environment)
9 error_message = "Environment must be development, staging, or production."
10 }
11}
12
13variable "instance_count" {
14 description = "Number of instances"
15 type = number
16 default = 1
17}
18
19variable "allowed_ips" {
20 description = "List of allowed IP addresses"
21 type = list(string)
22 default = []
23}
24
25variable "tags" {
26 description = "Resource tags"
27 type = map(string)
28 default = {}
29}
30
31# Usage
32resource "aws_instance" "web" {
33 count = var.instance_count
34 instance_type = var.environment == "production" ? "t3.large" : "t3.micro"
35
36 tags = merge(var.tags, {
37 Environment = var.environment
38 })
39}Outputs#
1# outputs.tf
2output "instance_ips" {
3 description = "Public IPs of web instances"
4 value = aws_instance.web[*].public_ip
5}
6
7output "load_balancer_dns" {
8 description = "Load balancer DNS name"
9 value = aws_lb.main.dns_name
10}
11
12output "database_endpoint" {
13 description = "Database connection endpoint"
14 value = aws_db_instance.main.endpoint
15 sensitive = true
16}State Management#
Remote State#
1# backend.tf
2terraform {
3 backend "s3" {
4 bucket = "my-terraform-state"
5 key = "prod/terraform.tfstate"
6 region = "us-east-1"
7 encrypt = true
8 dynamodb_table = "terraform-locks"
9 }
10}
11
12# Create state infrastructure first
13resource "aws_s3_bucket" "terraform_state" {
14 bucket = "my-terraform-state"
15
16 lifecycle {
17 prevent_destroy = true
18 }
19}
20
21resource "aws_s3_bucket_versioning" "terraform_state" {
22 bucket = aws_s3_bucket.terraform_state.id
23 versioning_configuration {
24 status = "Enabled"
25 }
26}
27
28resource "aws_dynamodb_table" "terraform_locks" {
29 name = "terraform-locks"
30 billing_mode = "PAY_PER_REQUEST"
31 hash_key = "LockID"
32
33 attribute {
34 name = "LockID"
35 type = "S"
36 }
37}State Commands#
1# View current state
2terraform state list
3terraform state show aws_instance.web
4
5# Move resources
6terraform state mv aws_instance.web aws_instance.web_server
7
8# Import existing resources
9terraform import aws_instance.web i-1234567890abcdef0
10
11# Remove from state (without destroying)
12terraform state rm aws_instance.webModules#
Creating a Module#
1# modules/vpc/main.tf
2variable "name" {
3 type = string
4}
5
6variable "cidr_block" {
7 type = string
8 default = "10.0.0.0/16"
9}
10
11variable "availability_zones" {
12 type = list(string)
13}
14
15resource "aws_vpc" "main" {
16 cidr_block = var.cidr_block
17 enable_dns_hostnames = true
18
19 tags = {
20 Name = var.name
21 }
22}
23
24resource "aws_subnet" "public" {
25 count = length(var.availability_zones)
26 vpc_id = aws_vpc.main.id
27 cidr_block = cidrsubnet(var.cidr_block, 8, count.index)
28 availability_zone = var.availability_zones[count.index]
29
30 tags = {
31 Name = "${var.name}-public-${count.index + 1}"
32 }
33}
34
35output "vpc_id" {
36 value = aws_vpc.main.id
37}
38
39output "public_subnet_ids" {
40 value = aws_subnet.public[*].id
41}Using Modules#
1# main.tf
2module "vpc" {
3 source = "./modules/vpc"
4
5 name = "production"
6 cidr_block = "10.0.0.0/16"
7 availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
8}
9
10module "web_cluster" {
11 source = "./modules/ecs-cluster"
12
13 name = "web"
14 vpc_id = module.vpc.vpc_id
15 subnet_ids = module.vpc.public_subnet_ids
16}
17
18# Registry modules
19module "vpc" {
20 source = "terraform-aws-modules/vpc/aws"
21 version = "5.0.0"
22
23 name = "production"
24 cidr = "10.0.0.0/16"
25}Workspaces#
1# Create environments
2terraform workspace new staging
3terraform workspace new production
4
5# Switch workspace
6terraform workspace select production
7
8# List workspaces
9terraform workspace list1# Use workspace in configuration
2locals {
3 environment = terraform.workspace
4
5 instance_type = {
6 development = "t3.micro"
7 staging = "t3.small"
8 production = "t3.large"
9 }
10}
11
12resource "aws_instance" "web" {
13 instance_type = local.instance_type[local.environment]
14}Best Practices#
Project Structure#
infrastructure/
├── environments/
│ ├── development/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ └── production/
├── modules/
│ ├── vpc/
│ ├── ecs-cluster/
│ └── rds/
└── global/
├── iam/
└── dns/
Naming Conventions#
1# Use consistent naming
2resource "aws_security_group" "web_server" { # snake_case
3 name = "web-server-sg" # kebab-case for AWS names
4
5 tags = {
6 Name = "web-server-sg"
7 Environment = var.environment
8 ManagedBy = "terraform"
9 Project = var.project
10 }
11}Prevent Accidental Destruction#
1resource "aws_db_instance" "main" {
2 # ...
3
4 lifecycle {
5 prevent_destroy = true
6 }
7}
8
9resource "aws_instance" "web" {
10 # ...
11
12 lifecycle {
13 create_before_destroy = true
14 }
15}CI/CD Integration#
1# GitHub Actions
2name: Terraform
3
4on:
5 pull_request:
6 paths:
7 - 'infrastructure/**'
8 push:
9 branches:
10 - main
11 paths:
12 - 'infrastructure/**'
13
14jobs:
15 plan:
16 runs-on: ubuntu-latest
17 steps:
18 - uses: actions/checkout@v3
19
20 - uses: hashicorp/setup-terraform@v2
21 with:
22 terraform_version: 1.6.0
23
24 - name: Terraform Init
25 run: terraform init
26 working-directory: infrastructure/environments/production
27
28 - name: Terraform Plan
29 run: terraform plan -out=tfplan
30 working-directory: infrastructure/environments/production
31
32 - name: Upload Plan
33 uses: actions/upload-artifact@v3
34 with:
35 name: tfplan
36 path: infrastructure/environments/production/tfplan
37
38 apply:
39 needs: plan
40 runs-on: ubuntu-latest
41 if: github.ref == 'refs/heads/main'
42 environment: production
43 steps:
44 - uses: actions/checkout@v3
45
46 - name: Download Plan
47 uses: actions/download-artifact@v3
48 with:
49 name: tfplan
50 path: infrastructure/environments/production
51
52 - name: Terraform Apply
53 run: terraform apply tfplan
54 working-directory: infrastructure/environments/productionConclusion#
Terraform enables reproducible, version-controlled infrastructure. Start with simple resources, graduate to modules for reusability, and use remote state for team collaboration.
Treat infrastructure code like application code: review changes, test in lower environments, and automate deployments. The investment in IaC pays dividends in reliability and speed.