TERRAFORM โ Infrastructure as Code
Last reviewed: 2026-06-16
Purpose: Comprehensive Terraform knowledge-base article covering installation, provider configuration, resource syntax, variables/outputs/locals, state management, modules, workspaces, CLI commands, troubleshooting, and a complete working example.
Overview
Terraform (by HashiCorp) is the industry-standard Infrastructure as Code (IaC) tool. It uses declarative configuration files to define, provision, and manage infrastructure across any cloud provider (AWS, Azure, GCP) or on-premises environment. Terraform is provider-agnostic โ the same workflow works for every platform.
Key concepts:
- Declarative: You describe the desired end state; Terraform figures out the steps.
- Immutable infrastructure: Resources are replaced rather than modified in-place when possible.
- State-driven: Terraform tracks the real-world state of resources in a state file (terraform.tfstate).
Installation
Linux (apt โ Debian/Ubuntu)
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
Linux (yum โ RHEL/CentOS/Fedora)
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum -y install terraform
macOS (Homebrew)
Windows (Chocolatey)
Verify installation
Enable tab completion (optional)
Provider Configuration โ AWS Example
Providers are plugins that Terraform uses to interact with cloud APIs. The required_providers block declares which providers and versions your configuration needs.
Minimal AWS provider (providers.tf)
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
Credentials
Terraform resolves AWS credentials in this order:
1. Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN
2. Shared credentials file: ~/.aws/credentials (profile set via AWS_PROFILE or profile attribute)
3. IAM instance profile (when running on EC2)
Recommended: use environment variables or AWS_PROFILE.
Multiple providers / aliases
Resource Syntax
Resources are the core building block in Terraform. They represent a single cloud object (e.g., EC2 instance, S3 bucket, IAM role).
Basic syntax
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "WebServer"
}
}
Pattern: resource "<provider_type>" "<local_name>" { ... }
Resource attributes and references
Refer to attributes of other resources using resource_type.local_name.attribute:
Data sources โ read existing resources
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-22.04-amd64-server-*"]
}
}
resource "aws_instance" "from_data" {
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
}
Variables, Outputs, and Locals
Input variables (variables.tf)
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
}
variable "region" {
description = "AWS region"
type = string
}
variable "tags" {
description = "Resource tags"
type = map(string)
default = {
Environment = "dev"
Project = "my-app"
}
}
Type constraints: string, number, bool, list(<type>), map(<type>), set(<type>), object({...}), tuple([...]), any.
Variable precedence (lowest to highest):
1. Default value (in variable block)
2. terraform.tfvars file
3. *.auto.tfvars files (alphabetically)
4. -var or -var-file CLI flags
Local values (locals.tf)
Locals are named expressions that don't vary between runs and don't need user input.
locals {
name_prefix = "${var.environment}-web-server"
common_tags = merge(var.tags, { Name = local.name_prefix })
}
Usage: local.name_prefix, local.common_tags.
Outputs (outputs.tf)
Outputs surface resource information after apply.
output "instance_id" {
description = "ID of the EC2 instance"
value = aws_instance.web.id
}
output "instance_public_ip" {
description = "Public IP of the EC2 instance"
value = aws_instance.web.public_ip
sensitive = false
}
output "db_password" {
description = "Database master password"
value = aws_db_instance.main.password
sensitive = true # hidden in CLI output
}
Use terraform output to retrieve outputs after apply.
State Management
State is Terraform's mapping of real-world resources back to your configuration. It lives in terraform.tfstate (JSON format).
Local state (default โ for development only)
Pros: simple, no external dependencies. Cons: no team sharing, no locking, easy to lose.
Remote state with S3 + DynamoDB (recommended for teams)
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-locks"
}
}
- S3 bucket: stores the state file. Enable versioning for rollback.
- DynamoDB table: provides state locking to prevent concurrent modifications (must have a primary key named
LockIDof typeString).
Create the DynamoDB lock table
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 \
--region us-east-1
S3 bucket setup
aws s3api create-bucket \
--bucket my-terraform-state-bucket \
--region us-east-1
aws s3api put-bucket-versioning \
--bucket my-terraform-state-bucket \
--versioning-configuration Status=Enabled
aws s3api put-bucket-encryption \
--bucket my-terraform-state-bucket \
--server-side-encryption-configuration '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
State commands
terraform state list # List all resources in state
terraform state show aws_instance.web # Show details of one resource
terraform state rm aws_instance.web # Remove a resource from state (without destroying)
terraform state mv aws_instance.web aws_instance.web_new # Rename/move a resource in state
terraform state pull > backup.tfstate # Download current state
terraform state push backup.tfstate # Upload state (dangerous โ use with care)
State file tips
- Never edit
terraform.tfstatemanually. Useterraform statesubcommands. - Commit state to Git? Only for local/single-user projects. For teams, always use remote state.
- Protect state โ it may contain secrets (plaintext passwords, keys). Use
sensitive = trueon outputs and encrypt S3.
Modules
Modules are reusable, encapsulated collections of .tf files that accept input variables and produce outputs.
Module structure
modules/
โโโ ec2-instance/
โโโ main.tf # resources
โโโ variables.tf # input variables
โโโ outputs.tf # outputs
Example module (modules/ec2-instance/main.tf)
variable "ami" { type = string }
variable "instance_type" { type = string }
variable "name" { type = string }
resource "aws_instance" "this" {
ami = var.ami
instance_type = var.instance_type
tags = { Name = var.name }
}
output "instance_id" { value = aws_instance.this.id }
output "public_ip" { value = aws_instance.this.public_ip }
output "security_groups" { value = aws_instance.this.security_groups }
Using a module in root config
module "web_server" {
source = "./modules/ec2-instance"
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
name = "web-server-01"
}
output "web_ip" {
value = module.web_server.public_ip
}
Module sources
| Source | Example |
|---|---|
| Local path | ./modules/ec2-instance |
| Git repo | git::https://github.com/org/repo.git//modules/ec2?ref=v1.0 |
| Terraform Registry | terraform-aws-modules/vpc/aws |
| S3 | s3::https://bucket/key.zip |
Workspaces
Workspaces allow you to manage multiple environments (dev, staging, prod) with the same configuration but separate state files.
Commands
terraform workspace new dev # Create and switch to 'dev'
terraform workspace new prod # Create and switch to 'prod'
terraform workspace list # List all workspaces (* = current)
terraform workspace show # Show current workspace
terraform workspace select dev # Switch to 'dev'
Using workspace in configuration
Default workspace
The default workspace is always present. Every workspace gets its own state file:
- terraform.tfstate.d/<workspace>/terraform.tfstate (local backend)
- <key>:<workspace>/terraform.tfstate (S3 backend)
Best practice: Use workspaces for environment separation OR separate directory trees (e.g., envs/dev/, envs/prod/ with a shared module), but not both in the same project.
CLI Commands โ Quick Reference
Core workflow
| Command | Purpose |
|---|---|
terraform init |
Initialize directory, download providers & modules, set up backend |
terraform plan |
Preview changes (read-only diff) |
terraform apply |
Execute the planned changes (prompts for confirmation) |
terraform destroy |
Tear down all resources in the configuration |
Formatting and validation
terraform fmt # Format all .tf files to canonical style
terraform fmt --recursive # Also format files in subdirectories
terraform validate # Check syntax and internal consistency
Plan and apply variations
terraform plan -out=plan.tfplan # Save plan to file
terraform apply plan.tfplan # Apply a saved plan (no prompt)
terraform apply -auto-approve # Skip confirmation prompt
terraform destroy -auto-approve # Destroy without confirmation
terraform plan -target=aws_instance.web # Plan only a specific resource
terraform apply -target=aws_instance.web # Apply only a specific resource
Inspection
terraform show # Show state or plan file contents
terraform show plan.tfplan # Show a saved plan
terraform graph # Generate DOT graph of dependency tree
terraform providers # List required providers
terraform version # Show Terraform version
Workspace
terraform workspace list # List workspaces
terraform workspace new <name> # Create workspace
terraform workspace select <name> # Switch workspace
terraform workspace show # Show current workspace
State management
terraform state list # List tracked resources
terraform state show <address> # Show details of one resource
terraform state rm <address> # Remove resource from state
terraform state mv <old> <new> # Rename resource in state
terraform state pull # Download state to stdout
terraform state push <file> # Upload state (dangerous)
taint / untaint (legacy)
terraform taint aws_instance.web # Mark for recreation on next apply
terraform untaint aws_instance.web # Remove taint mark
Note:
terraform taintis legacy. Prefer-replace:
Import existing resources
Troubleshooting Tips
Problem: terraform init fails with "Failed to query available provider packages"
Causes & fixes:
- No internet access / air-gapped: Mirror providers locally with terraform providers mirror /path/to/mirror, then configure provider_installation in .terraformrc.
- Invalid source address: Ensure source = "hashicorp/aws" format (not "aws").
- Version constraint too tight: Relax version = "~> 5.0" to ">= 4.0, < 6.0".
Problem: "Error creating resource: ... status code: 403" / "AccessDenied"
Fix: Check IAM permissions. The user/role must have the necessary actions allowed (e.g., ec2:RunInstances, s3:CreateBucket).
Problem: State lock error "Failed to acquire state lock"
Causes & fixes: - Another apply is running: Wait for it to finish. - Stale lock (stale process died):
The lock ID is shown in the error message.Problem: "Resource already exists" during apply
Fix: Import the existing resource:
Problem: terraform plan shows changes you didn't intend
Common causes:
- Drift: Someone modified the resource outside Terraform (e.g., via AWS Console). Run terraform apply to reconcile.
- Missing lifecycle meta-argument:
count or for_each causing resource index shifts. Prefer for_each over count for stable addresses:
Problem: "Invalid template" or "Invalid interpolation"
Fix: Terraform v0.12+ uses var.name (not "${var.name}"). The old ${} syntax still works for string interpolation but is deprecated inside resource attributes.
Problem: Secrets exposed in state / output
Fix:
1. Mark outputs as sensitive = true.
2. Enable S3 bucket encryption + restrict IAM access to state.
3. Use a secrets manager (AWS Secrets Manager, Vault) instead of plaintext variables:
Problem: Provider version conflicts across modules
Fix: Use a root-level required_providers block with version constraints that satisfy all modules. Avoid pinning to exact versions โ use ~> x.y (allows patch bumps).
Complete Working Example โ EC2 Instance with S3 Backend
Below is a self-contained, production-style Terraform configuration that provisions an EC2 instance with an S3 bucket and uses remote state.
Directory layout
terraform-example/
โโโ main.tf
โโโ variables.tf
โโโ outputs.tf
โโโ terraform.tfbackend # partial backend config (CLI-injected)
main.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
# --- Provider ---
provider "aws" {
region = var.region
}
# --- S3 Bucket ---
resource "aws_s3_bucket" "data" {
bucket = var.bucket_name
force_destroy = true
}
resource "aws_s3_bucket_versioning" "data" {
bucket = aws_s3_bucket.data.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
bucket = aws_s3_bucket.data.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
# --- Security Group ---
resource "aws_security_group" "web_sg" {
name_prefix = "${var.environment}-web-sg-"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "SSH"
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTP"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.environment}-web-sg"
Environment = var.environment
}
}
# --- EC2 Instance ---
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
vpc_security_group_ids = [aws_security_group.web_sg.id]
key_name = var.key_name
root_block_device {
volume_size = 20
volume_type = "gp3"
}
tags = {
Name = "${var.environment}-web-server"
Environment = var.environment
}
lifecycle {
ignore_changes = [ami]
}
}
# --- Data Source ---
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
variables.tf
variable "region" {
description = "AWS region"
type = string
default = "us-east-1"
}
variable "environment" {
description = "Environment name (dev/staging/prod)"
type = string
default = "dev"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
}
variable "bucket_name" {
description = "Globally unique S3 bucket name"
type = string
}
variable "key_name" {
description = "EC2 key pair name for SSH access"
type = string
default = null
}
outputs.tf
output "instance_id" {
description = "EC2 instance ID"
value = aws_instance.web.id
}
output "instance_public_ip" {
description = "EC2 instance public IP"
value = aws_instance.web.public_ip
}
output "bucket_name" {
description = "S3 bucket name"
value = aws_s3_bucket.data.bucket
}
output "instance_ssh_command" {
description = "SSH command to connect to the instance"
value = "ssh -i ${var.key_name != null ? var.key_name : "<key>"}.pem ubuntu@${aws_instance.web.public_ip}"
}
terraform.tfbackend โ partial backend config (optional, for CI)
bucket = "my-terraform-state-bucket"
key = "dev/ec2-example/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-locks"
Usage
# 1. Initialize (with remote backend)
terraform init -backend-config=terraform.tfbackend
# 2. Set variables
# Option A: terraform.tfvars
# Option B: environment variables
export TF_VAR_bucket_name="my-unique-bucket-2026"
export TF_VAR_environment="dev"
export TF_VAR_key_name="my-key-pair"
# 3. Plan
terraform plan -out=plan.tfplan
# 4. Apply
terraform apply plan.tfplan
# 5. Verify outputs
terraform output
# 6. Destroy when done
terraform destroy -auto-approve
Notes
- Always run
terraform fmtandterraform validatebefore committing code. - Use remote state with S3 + DynamoDB for all team/CI workflows.
- Pin provider versions in
required_providersbut leave room for patches (~> x.y). - Prefer
for_eachovercountfor stable resource addressing when iterating over maps/sets. - Use
-replaceflag instead ofterraform taint(which is legacy). - Store secrets in a secrets manager (AWS Secrets Manager, Vault) โ never commit them to
.tfvarsfiles. - Terraform Registry (registry.terraform.io) contains thousands of community and official modules for common patterns (VPC, RDS, EKS, etc.).
- For deeper cloud infrastructure topics, see the
AWSmodule which covers EC2, S3, and IAM in more detail.