Terraform: Infrastructure as Code Complete Guide (2026)

Terraform is the most widely adopted IaC tool — it works across AWS, Azure, GCP, and 3,000+ providers using a declarative HCL syntax. You describe what infrastructure you want, Terraform figures out how to create it. This guide covers HCL fundamentals, state management, modules, and production best practices.

1. Installation and Setup

# Install Terraform (macOS)
brew tap hashicorp/tap
brew install hashicorp/tap/terraform

# Windows (Chocolatey)
choco install terraform

# Verify
terraform version

# Configure AWS credentials (Terraform uses standard AWS credential chain)
export AWS_PROFILE=myapp-prod
# Or use OIDC with GitHub Actions / EC2 instance role — no static keys

2. HCL Syntax Basics

# main.tf — a complete VPC example
terraform {
  required_version = ">= 1.7"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
  default_tags {
    tags = {
      Project     = var.project_name
      Environment = var.environment
      ManagedBy   = "terraform"
    }
  }
}

# Resource block
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "${var.project_name}-vpc"
  }
}

resource "aws_subnet" "private" {
  count             = length(var.availability_zones)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
  availability_zone = var.availability_zones[count.index]
}

# Data source — look up existing resources
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

3. State Management

Terraform state tracks what resources it manages. Never use local state in a team — use a remote backend:

# backend.tf — S3 backend with DynamoDB locking
terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "production/main.tfstate"
    region         = "us-east-1"
    encrypt        = true
    kms_key_id     = "arn:aws:kms:us-east-1:123456789:key/abc123"
    dynamodb_table = "terraform-state-locks"
  }
}
# Create the S3 bucket and DynamoDB table for state management
aws s3api create-bucket --bucket mycompany-terraform-state --region us-east-1
aws s3api put-bucket-versioning --bucket mycompany-terraform-state \
  --versioning-configuration Status=Enabled
aws s3api put-bucket-encryption --bucket mycompany-terraform-state \
  --server-side-encryption-configuration '...'

aws dynamodb create-table \
  --table-name terraform-state-locks \
  --attribute-definitions AttributeName=LockID,AttributeType=S \
  --key-schema AttributeName=LockID,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST

4. Variables and Outputs

# variables.tf
variable "aws_region" {
  type        = string
  default     = "us-east-1"
  description = "AWS region to deploy into"
}

variable "environment" {
  type        = string
  validation {
    condition     = contains(["staging", "production"], var.environment)
    error_message = "Environment must be staging or production."
  }
}

variable "db_password" {
  type      = string
  sensitive = true  # masked in plan output and state
}

# outputs.tf
output "vpc_id" {
  value       = aws_vpc.main.id
  description = "VPC ID for use in other stacks"
}

output "alb_dns_name" {
  value = aws_lb.main.dns_name
}
# Pass variables
terraform plan -var="environment=production" -var-file="production.tfvars"

# production.tfvars
environment = "production"
aws_region  = "us-east-1"
instance_type = "m6i.large"

5. Modules

# modules/rds/main.tf — reusable RDS module
variable "identifier" {}
variable "instance_class" { default = "db.t3.medium" }
variable "db_name" {}
variable "master_password" { sensitive = true }
variable "subnet_ids" { type = list(string) }
variable "vpc_security_group_ids" { type = list(string) }

resource "aws_db_instance" "this" {
  identifier          = var.identifier
  engine              = "postgres"
  engine_version      = "16.2"
  instance_class      = var.instance_class
  db_name             = var.db_name
  username            = "adminuser"
  password            = var.master_password
  db_subnet_group_name = aws_db_subnet_group.this.name
  vpc_security_group_ids = var.vpc_security_group_ids
  multi_az            = true
  storage_encrypted   = true
  skip_final_snapshot = false
}

output "endpoint" { value = aws_db_instance.this.endpoint }

# Calling the module
module "app_db" {
  source = "./modules/rds"
  identifier    = "myapp-production"
  db_name       = "myapp"
  master_password = var.db_password
  subnet_ids    = module.vpc.private_subnet_ids
  vpc_security_group_ids = [aws_security_group.rds.id]
}

6. for_each and count

# for_each — preferred over count for resources that may be removed
variable "s3_buckets" {
  default = {
    assets    = { versioning = true }
    backups   = { versioning = true }
    artifacts = { versioning = false }
  }
}

resource "aws_s3_bucket" "this" {
  for_each = var.s3_buckets
  bucket   = "mycompany-${each.key}"
}

resource "aws_s3_bucket_versioning" "this" {
  for_each = { for k, v in var.s3_buckets : k => v if v.versioning }
  bucket   = aws_s3_bucket.this[each.key].id
  versioning_configuration {
    status = "Enabled"
  }
}

7. Workflow

terraform init           # download providers and modules
terraform validate       # check syntax
terraform fmt -recursive # format all .tf files
terraform plan -out=tfplan.binary  # create plan, save to file
terraform show tfplan.binary       # review the plan
terraform apply tfplan.binary      # apply saved plan (no prompt)
terraform output                   # show outputs
terraform destroy                  # destroy all resources (careful!)

Frequently Asked Questions

What is OpenTofu?

OpenTofu is the open-source fork of Terraform created after HashiCorp changed Terraform's license from MPL 2.0 to BSL in 2023. It is governed by the Linux Foundation and is a drop-in replacement for Terraform. Most Terraform code runs on OpenTofu without changes.

How do I import existing AWS resources into Terraform?

Use terraform import resource_type.name resource_id (e.g., terraform import aws_s3_bucket.assets my-bucket-name). In Terraform 1.5+, use the import block in HCL for version-controlled imports. Then run terraform plan and reconcile any drift between actual state and your HCL.

How do I handle secrets in Terraform?

Never put secrets in .tf files or tfvars committed to git. Options: pass via environment variables (TF_VAR_db_password), use Vault provider to read secrets at apply time, use AWS Secrets Manager data source to read secrets during plan/apply, or use SOPS to encrypt tfvars files.