Infrastructure as Code tool for provisioning and managing cloud infrastructure declaratively.

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