Infrastructure as Code tool for provisioning and managing cloud infrastructure declaratively. Commonly used with AWS, GCP, and Azure.

Consider OpenTofu as an open source alternative (fork after HashiCorp licence change).

Basic Commands

terraform init        # Initialise working directory, download providers
terraform plan        # Preview changes without applying
terraform apply       # Apply changes to infrastructure
terraform destroy     # Destroy all managed infrastructure
terraform fmt --recursive   # Format all .tf files recursively
terraform validate          # Validate configuration syntax
terraform refresh           # Update state to match real infrastructure
terraform output            # Display output values

State

Terraform tracks infrastructure in a state file. This is the source of truth for what Terraform manages.

Backends

Configure where state is stored:

  • local - Default, stores state in terraform.tfstate file
  • s3 - AWS S3 bucket (recommended for AWS)
  • gcs - Google Cloud Storage
  • azurerm - Azure Blob Storage
  • remote - Terraform Cloud/Enterprise

Remote backends provide:

  • Collaboration (shared state)
  • State locking (prevents concurrent modifications)
  • Encryption at rest
  • Versioning/history

State Commands

terraform show                           # Display current state
terraform state list                     # List all resources in state
terraform state show <resource>          # Show details of a resource
terraform state mv <src> <dst>           # Move/rename resource in state
terraform state rm <resource>            # Remove resource from state (doesn't destroy)
terraform import <resource> <id>         # Import existing infrastructure into state
terraform state pull                     # Download and output remote state
terraform state push                     # Upload local state to remote

State Locking

Most remote backends support locking to prevent concurrent operations. If a lock is stuck:

terraform force-unlock <lock_id>

Variables

Input Variables

Define in configuration:

variable "region" {
  type        = string
  default     = "eu-west-2"
  description = "AWS region to deploy to"
  
  validation {
    condition     = can(regex("^eu-", var.region))
    error_message = "Region must be in EU."
  }
}

Set values (in order of precedence, lowest to highest):

  1. Default value in variable block
  2. terraform.tfvars or *.auto.tfvars files (auto-loaded)
  3. -var-file="env.tfvars" flag
  4. -var="region=eu-west-1" flag
  5. TF_VAR_region environment variable
terraform apply -var="gcp_project_id=my-project"
terraform apply -var-file="production.tfvars"

Output Variables

Expose values from your configuration:

output "instance_ip" {
  value       = aws_instance.web.public_ip
  description = "Public IP of the web server"
  sensitive   = false
}

Local Values

Assign names to expressions for reuse:

locals {
  environment = "production"
  common_tags = {
    Environment = local.environment
    ManagedBy   = "Terraform"
  }
}

Loops and Conditionals

count

Create multiple instances of a resource:

resource "aws_instance" "server" {
  count = 3
  ami   = "ami-12345"
  tags = {
    Name = "server-${count.index}"
  }
}

for_each

Iterate over maps or sets:

resource "aws_instance" "server" {
  for_each = toset(["web", "api", "worker"])
  ami      = "ami-12345"
  tags = {
    Name = each.value
    Key  = each.key
  }
}

Prefer for_each over count for resources that may be added/removed - it references by key rather than index, avoiding recreation of resources.

for Expressions

Transform collections:

locals {
  upper_names = [for name in var.names : upper(name)]
  name_map    = { for name in var.names : name => upper(name) }
}

Conditional Expressions

Ternary operator:

resource "aws_instance" "server" {
  instance_type = var.environment == "production" ? "m5.large" : "t3.micro"
}

Conditional resource creation:

resource "aws_instance" "server" {
  count = var.create_instance ? 1 : 0
}

dynamic Blocks

Generate repeated nested blocks:

dynamic "ingress" {
  for_each = var.ports
  content {
    from_port = ingress.value
    to_port   = ingress.value
    protocol  = "tcp"
  }
}

Modules

Modules are reusable, encapsulated Terraform configurations.

Using Modules

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"
  
  name = "my-vpc"
  cidr = "10.0.0.0/16"
}

Module Sources

  • Local path: source = "./modules/vpc"
  • Terraform Registry: source = "hashicorp/consul/aws"
  • GitHub: source = "github.com/org/repo//modules/vpc"
  • S3: source = "s3::https://bucket.s3.amazonaws.com/module.zip"
  • GCS: source = "gcs::https://bucket.storage.googleapis.com/module.zip"

Module Outputs

Access module outputs:

resource "aws_instance" "web" {
  subnet_id = module.vpc.public_subnet_ids[0]
}

Workspaces

Manage multiple environments with the same configuration:

terraform workspace list              # List workspaces
terraform workspace new staging       # Create new workspace
terraform workspace select production # Switch workspace
terraform workspace delete staging    # Delete workspace

Access current workspace in configuration:

locals {
  environment = terraform.workspace
}

Best Practices

File Structure

project/
├── main.tf          # Main resources
├── variables.tf     # Variable declarations
├── outputs.tf       # Output declarations
├── providers.tf     # Provider configuration
├── versions.tf      # Required versions
├── terraform.tfvars # Variable values (don't commit secrets)
└── modules/
    └── vpc/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

General

  • Use remote state with locking for team collaboration
  • Never commit .tfstate files or .tfvars with secrets
  • Pin provider and module versions
  • Use terraform plan before apply in CI/CD
  • Tag all resources consistently
  • Use data sources to reference existing infrastructure
  • Keep modules small and focused

.gitignore

*.tfstate
*.tfstate.*
*.tfvars
.terraform/
.terraform.lock.hcl

Resources

Cheat Sheets

Documentation

Learning