Most developers interact with cloud infrastructure through web consoles, clicking through forms to provision databases, set up load balancers, and configure networking. This works until you need to reproduce the environment, onboard a teammate, recover from a disaster, or understand what changed last Tuesday. Terraform solves this by letting you define infrastructure in declarative code files that you version, review, and deploy like any other software. If you can write a config file, you can write Terraform.
What Terraform Actually Does
Terraform reads .tf files written in HCL (HashiCorp Configuration Language), compares the desired state you described against the actual state of your cloud infrastructure, and makes API calls to reconcile the difference. Add a new resource to your file, run terraform apply, and Terraform creates it. Remove a resource, apply again, and Terraform destroys it. Change a property, and Terraform updates it in place or recreates it as needed. The state file tracks what Terraform manages, preventing it from touching resources you created elsewhere.
Your First Terraform Configuration
# main.tf
terraform {
required_version = ">= 1.9"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
backend "s3" {
bucket = "mycompany-terraform-state"
key = "prod/app/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "terraform"
Project = var.project_name
}
}
}
# variables.tf
variable "aws_region" {
description = "AWS region for all resources"
type = string
default = "us-east-1"
}
variable "environment" {
description = "Deployment environment"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "project_name" {
type = string
default = "myapp"
}
# storage.tf
resource "aws_s3_bucket" "app_assets" {
bucket = "${var.project_name}-${var.environment}-assets"
}
resource "aws_s3_bucket_versioning" "app_assets" {
bucket = aws_s3_bucket.app_assets.id
versioning_configuration { status = "Enabled" }
}
resource "aws_s3_bucket_server_side_encryption_configuration" "app_assets" {
bucket = aws_s3_bucket.app_assets.id
rule {
apply_server_side_encryption_by_default { sse_algorithm = "AES256" }
}
}
resource "aws_s3_bucket_public_access_block" "app_assets" {
bucket = aws_s3_bucket.app_assets.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
The Core Workflow
terraform init # Download providers, configure backend
terraform plan -var="environment=dev" # Preview changes
terraform apply -var="environment=dev" # Apply with confirmation
terraform destroy -var="environment=dev" # Tear everything down
The plan step is critical. It shows you exactly what Terraform will create, modify, or destroy before it touches anything. Treat it like a diff review. In CI/CD pipelines, run plan on pull requests and apply only on merge to main.
Modules: Reusable Infrastructure Packages
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.5.0"
name = "${var.project_name}-${var.environment}"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
single_nat_gateway = var.environment != "prod"
}
resource "aws_instance" "app" {
subnet_id = module.vpc.private_subnets[0]
}
State Management and Team Workflows
Terraform tracks what it manages in a state file. For teams, store state remotely (S3 + DynamoDB locking, Terraform Cloud, or GCS) so everyone works from the same source of truth. Never commit terraform.tfstate to Git. Use workspaces or separate state files per environment to prevent accidental cross-environment changes.
Common Mistakes to Avoid
Hardcoding values. Use variables for anything that differs between environments. Ignoring the plan output. A property change on an RDS instance might trigger a replacement, which means downtime and data loss. Monolithic configurations. Split resources into logical files. Not pinning provider versions. An unpinned provider update can introduce breaking changes.
Terraform is not just an ops tool. If you deploy code that runs on cloud infrastructure, understanding how that infrastructure is provisioned makes you a more effective engineer. Start with a single resource, run plan and apply, and build from there.
Further reading: Terraform Documentation | Terraform Registry | HashiCorp Tutorials

Leave a Reply