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.
Table of Contents
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.