Clicking through cloud consoles doesn’t scale. Terraform lets you define infrastructure in code, track changes in git, and deploy the same environment repeatedly.
Core Concepts#
- Provider: Plugin for a platform (AWS, GCP, Azure, etc.)
- Resource: A thing to create (server, database, DNS record)
- State: Terraform’s record of what exists
- Plan: Preview of changes before applying
- Apply: Make the changes happen
Basic Workflow#
1
2
3
4
| terraform init # Download providers
terraform plan # Preview changes
terraform apply # Create/update resources
terraform destroy # Tear everything down
|
First Configuration#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| # main.tf
# Configure the AWS provider
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
# Create an EC2 instance
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "WebServer"
}
}
|
1
2
3
| terraform init # Downloads AWS provider
terraform plan # Shows: 1 to add
terraform apply # Creates the instance
|
Variables#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| # variables.tf
variable "environment" {
description = "Deployment environment"
type = string
default = "dev"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "allowed_ips" {
description = "IPs allowed to SSH"
type = list(string)
default = ["0.0.0.0/0"]
}
# main.tf - use variables
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = var.instance_type
tags = {
Name = "web-${var.environment}"
Environment = var.environment
}
}
|
Setting Variables#
1
2
3
4
5
6
7
8
9
| # Command line
terraform apply -var="environment=prod"
# File (terraform.tfvars)
environment = "prod"
instance_type = "t3.small"
# Environment variables
export TF_VAR_environment="prod"
|
Outputs#
1
2
3
4
5
6
7
8
9
10
| # outputs.tf
output "instance_ip" {
description = "Public IP of the instance"
value = aws_instance.web.public_ip
}
output "instance_id" {
description = "Instance ID"
value = aws_instance.web.id
}
|
After apply:
1
2
| terraform output instance_ip
# 54.123.45.67
|
Data Sources#
Query existing resources:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # Find latest Ubuntu AMI
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
}
|
Resource Dependencies#
Terraform figures out order automatically:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # VPC first
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
# Then subnet (references VPC)
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
# Then instance (references subnet)
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
}
|
Explicit dependency when needed:
1
2
3
4
| resource "aws_instance" "web" {
# ...
depends_on = [aws_iam_role_policy.s3_access]
}
|
Modules#
Reusable infrastructure components:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # modules/web-server/main.tf
variable "instance_type" {
default = "t3.micro"
}
variable "environment" {}
resource "aws_instance" "this" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
tags = {
Environment = var.environment
}
}
output "public_ip" {
value = aws_instance.this.public_ip
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # main.tf - use the module
module "web_prod" {
source = "./modules/web-server"
environment = "prod"
instance_type = "t3.small"
}
module "web_staging" {
source = "./modules/web-server"
environment = "staging"
}
output "prod_ip" {
value = module.web_prod.public_ip
}
|
State Management#
Terraform tracks what it created in state files.
Remote State (Required for Teams)#
1
2
3
4
5
6
7
8
9
10
| # backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
|
State Commands#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # List resources in state
terraform state list
# Show resource details
terraform state show aws_instance.web
# Remove from state (doesn't destroy)
terraform state rm aws_instance.web
# Import existing resource
terraform import aws_instance.web i-1234567890abcdef0
# Move resource (rename)
terraform state mv aws_instance.old aws_instance.new
|
Workspaces#
Multiple environments with same code:
1
2
3
4
| terraform workspace new staging
terraform workspace new prod
terraform workspace select staging
terraform workspace list
|
1
2
3
4
5
6
7
8
| # Use workspace name in config
resource "aws_instance" "web" {
instance_type = terraform.workspace == "prod" ? "t3.small" : "t3.micro"
tags = {
Environment = terraform.workspace
}
}
|
Loops and Conditionals#
count#
1
2
3
4
5
6
7
8
9
| resource "aws_instance" "web" {
count = 3
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
tags = {
Name = "web-${count.index + 1}"
}
}
|
for_each#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| variable "instances" {
default = {
web1 = "t3.micro"
web2 = "t3.small"
api = "t3.medium"
}
}
resource "aws_instance" "servers" {
for_each = var.instances
ami = data.aws_ami.ubuntu.id
instance_type = each.value
tags = {
Name = each.key
}
}
|
Conditionals#
1
2
3
4
5
6
7
8
| # Create only if condition is true
resource "aws_instance" "bastion" {
count = var.create_bastion ? 1 : 0
# ...
}
# Conditional value
instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
|
Lifecycle Rules#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| resource "aws_instance" "web" {
# ...
lifecycle {
# Don't destroy before creating replacement
create_before_destroy = true
# Ignore changes to tags
ignore_changes = [tags]
# Never destroy this resource
prevent_destroy = true
}
}
|
Practical Example#
Complete web infrastructure:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
| # VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = { Name = "${var.project}-vpc" }
}
# Public subnet
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = true
tags = { Name = "${var.project}-public" }
}
# Internet gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
# Route table
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
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
# Security group
resource "aws_security_group" "web" {
name = "${var.project}-web-sg"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
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"]
}
}
# EC2 instance
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web.id]
tags = {
Name = "${var.project}-web"
Environment = var.environment
}
}
|
Best Practices#
- Never commit state files — use remote state
- Use variables — no hardcoded values
- Format code —
terraform fmt - Validate before apply —
terraform validate - Use modules — DRY infrastructure
- Lock provider versions — reproducible builds
- Review plans carefully — especially destroys
Quick Reference#
1
2
3
4
5
6
7
8
9
| terraform init # Initialize
terraform plan # Preview
terraform apply # Deploy
terraform destroy # Tear down
terraform fmt # Format code
terraform validate # Check syntax
terraform output # Show outputs
terraform state list # List resources
terraform import # Import existing
|
Terraform turns infrastructure into code you can review, version, and collaborate on. Start small, build modules, and watch your deployments become reproducible.
Try It Yourself#
Want to practice Terraform? These cloud platforms offer free credits to get started:
- DigitalOcean — $200 free credit for 60 days. Simple, developer-friendly VPS hosting.
- Vultr — $100 free credit. Great for global deployments.
- AWS Free Tier — 12 months of free services including EC2, S3, and RDS.
Disclosure: Some links are affiliate links. You get free credits, I get a small commission. Win-win.