Skip to content

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)

brew tap hashicorp/tap
brew install hashicorp/tap/terraform

Windows (Chocolatey)

choco install terraform

Verify installation

terraform --version

Enable tab completion (optional)

terraform -install-autocomplete

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.

export AWS_PROFILE=my-profile

Multiple providers / aliases

provider "aws" {
  region = "us-east-1"
}

provider "aws" {
  alias  = "west"
  region = "us-west-2"
}

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:

resource "aws_eip" "web_ip" {
  instance = aws_instance.web.id
}

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

terraform apply -var="instance_type=t3.small" -var-file="prod.tfvars"

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)

terraform {
  backend "local" {
    path = "terraform.tfstate"
  }
}

Pros: simple, no external dependencies. Cons: no team sharing, no locking, easy to lose.

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 LockID of type String).

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.tfstate manually. Use terraform state subcommands.
  • 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 = true on 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

resource "aws_s3_bucket" "app" {
  bucket = "my-app-${terraform.workspace}"
}

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 taint is legacy. Prefer -replace:

terraform apply -replace=aws_instance.web

Import existing resources

terraform import aws_instance.web i-1234567890abcdef0  # Import existing resource into state

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):

terraform force-unlock <LOCK_ID>
The lock ID is shown in the error message.

Problem: "Resource already exists" during apply

Fix: Import the existing resource:

terraform import aws_instance.web i-1234567890abcdef0

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:

lifecycle {
  ignore_changes = [tags, user_data]
}
- Incorrect count or for_each causing resource index shifts. Prefer for_each over count for stable addresses:
resource "aws_instance" "web" {
  for_each = toset(var.instance_names)
  # ...
}

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:

data "aws_secretsmanager_secret_version" "db_pass" {
  secret_id = "my-db-password"
}

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 fmt and terraform validate before committing code.
  • Use remote state with S3 + DynamoDB for all team/CI workflows.
  • Pin provider versions in required_providers but leave room for patches (~> x.y).
  • Prefer for_each over count for stable resource addressing when iterating over maps/sets.
  • Use -replace flag instead of terraform taint (which is legacy).
  • Store secrets in a secrets manager (AWS Secrets Manager, Vault) โ€” never commit them to .tfvars files.
  • 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 AWS module which covers EC2, S3, and IAM in more detail.