What Is Infrastructure as Code?
Infrastructure as Code (IaC) is the practice of managing and provisioning infrastructure through machine-readable configuration files rather than manual processes or interactive configuration tools. Instead of clicking through a cloud console, you write code that defines your servers, networks, databases, and more.
The benefits are clear: version-controlled infrastructure, repeatable deployments, peer-reviewed changes through pull requests, and the ability to destroy and recreate environments with a single command.
Why Terraform?
Terraform, now part of HashiCorp's ecosystem, is one of the most popular IaC tools. It uses a declarative language called HCL (HashiCorp Configuration Language) to describe desired infrastructure state. Terraform then computes the minimal set of changes needed to reach that state — and applies them safely.
Key strengths include:
- Multi-cloud support — AWS, Azure, GCP, and 150+ providers via providers and registries.
- State management — Tracks the real-world state of your infrastructure to plan accurate changes.
- Plan/apply workflow — Review changes before they happen, preventing accidental modifications.
Project Structure
A well-organized Terraform project separates concerns by environment and resource type:
infrastructure/
├── main.tf # Root module - providers and shared resources
├── variables.tf # Input variable definitions
├── outputs.tf # Output values
├── modules/
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── ecs/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── environments/
├── dev/
│ └── main.tf
└── prod/
└── main.tf
Defining Your First Resources
Here's a minimal example that creates an AWS S3 bucket and an EC2 instance:
provider "aws" {
region = "us-east-1"
}
resource "aws_s3_bucket" "app_data" {
bucket = "my-app-data-${var.environment}"
tags = {
Name = "Application Data"
Environment = var.environment
}
}
resource "aws_instance" "app_server" {
ami = var.ami_id
instance_type = "t3.micro"
vpc_security_group_ids = [aws_security_group.app.id]
subnet_id = var.subnet_id
tags = {
Name = "app-server-${var.environment}"
}
}
resource "aws_security_group" "app" {
name_prefix = "app-sg-"
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"]
}
}
Variables and Outputs
Keep values flexible with variables:
variable "environment" {
description = "Deployment environment"
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "ami_id" {
description = "AMI ID for the EC2 instance"
type = string
}
Expose useful values as outputs:
output "bucket_arn" {
description = "ARN of the S3 bucket"
value = aws_s3_bucket.app_data.arn
}
output "instance_public_ip" {
description = "Public IP of the app server"
value = aws_instance.app_server.public_ip
}
The Workflow: Init, Plan, Apply
# Initialize the working directory, downloading providers and modules
terraform init
# Preview what changes Terraform will make
terraform plan -var-file=environments/dev.tfvars
# Apply the planned changes
terraform apply -var-file=environments/dev.tfvars
# Destroy all managed resources
terraform destroy
State Management Best Practices
Terraform's state file is the source of truth. Never edit it manually. Store it remotely for team collaboration:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "dev/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-lock"
}
}
The S3 bucket stores the state, and DynamoDB provides state locking to prevent concurrent modifications.
Conclusion
Terraform transforms infrastructure management from a fragile, manual process into a reliable, automated practice. Start with a single resource, learn the plan/apply cycle, and gradually expand your IaC footprint. The discipline of treating infrastructure as code pays dividends in reliability, collaboration, and speed.