Infrastructure as Code tool by HashiCorp for provisioning and managing cloud and on-premises resources through declarative configuration files.
Addresses below are RFC 5737 documentation ranges or placeholders - swap in your own.
Table of Contents#
- Overview
- Core Concepts
- Setting Up Terraform
- 3.1 Prerequisites
- 3.2 Installation
- 3.3 Project Structure
- HCL Configuration Language
- Modules
- State Management
- Terraform Commands
- CI/CD Integration
- Troubleshooting
- Sources
1. Overview#
Terraform is an open-source tool created by HashiCorp that allows users to define and provision infrastructure through a high-level configuration language called HashiCorp Configuration Language (HCL). It implements the principle of Infrastructure as Code (IaC), enabling the safe and predictable management of cloud services, on-premises resources, and SaaS platforms in a declarative format.
Key characteristics:
- Declarative - you define the desired end state, Terraform determines the steps to reach it
- Provider-based - supports AWS, Azure, GCP, Proxmox, VMware, Kubernetes, and hundreds more via plugins
- State-driven - tracks the real-world state of infrastructure to calculate minimal changes
- Plan before apply - always shows a preview of changes before making them
- Idempotent - applying the same configuration produces the same result
2. Core Concepts#
2.1 Infrastructure as Code#
IaC is a key DevOps practice that involves managing and provisioning infrastructure through code instead of manual processes. Benefits include:
- Consistency - identical environments every time
- Version control - track and review infrastructure changes like application code
- Reproducibility - create identical stacks for dev, staging, and production
- Automation - eliminate manual provisioning steps
2.2 Terraform Workflow#
The core workflow follows three stages:
- Write - define infrastructure in
.tfconfiguration files using HCL - Plan - run
terraform planto preview changes (what will be created, modified, or destroyed) - Apply - run
terraform applyto execute the changes and update the state
Write .tf files -> terraform init -> terraform plan -> terraform apply
| |
Review changes State updated3. Setting Up Terraform#
3.1 Prerequisites#
- A supported operating system (Linux, macOS, Windows)
- An account with a cloud provider or access to a virtualization platform (Proxmox, VMware, etc.)
- Access to a command-line interface
3.2 Installation#
On Linux (manual):
# Download and install
wget https://releases.hashicorp.com/terraform/<version>/terraform_<version>_linux_amd64.zip
unzip terraform_<version>_linux_amd64.zip
sudo mv terraform /usr/local/bin/
terraform --versionOn Arch Linux:
sudo pacman -S terraformOn macOS (Homebrew):
brew install terraform3.3 Project Structure#
A typical Terraform project layout:
my-infrastructure/
├── main.tf # Primary resource definitions
├── variables.tf # Input variable declarations
├── outputs.tf # Output value declarations
├── providers.tf # Provider configuration
├── terraform.tfvars # Variable values (do not commit secrets)
├── backend.tf # State backend configuration
└── modules/
└── webserver/
├── main.tf
├── variables.tf
└── outputs.tf4. HCL Configuration Language#
4.1 Providers#
Providers are plugins that interact with APIs of cloud platforms, SaaS services, and other infrastructure. Each provider adds a set of resource types and data sources.
# providers.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
proxmox = {
source = "Telmate/proxmox"
version = "~> 3.0"
}
}
}
provider "aws" {
region = var.aws_region
profile = var.aws_profile
}
provider "proxmox" {
pm_api_url = "https://proxmox.example.com:8006/api2/json"
pm_api_token_id = var.proxmox_token_id
pm_api_token_secret = var.proxmox_token_secret
pm_tls_insecure = true
}4.2 Resources#
Resources are the most important element in Terraform. Each resource block describes one or more infrastructure objects.
# main.tf - AWS example
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = {
Name = "main-vpc"
Environment = var.environment
}
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "${var.aws_region}a"
map_public_ip_on_launch = true
tags = {
Name = "public-subnet"
}
}
resource "aws_instance" "web" {
count = var.instance_count
ami = var.ami_id
instance_type = var.instance_type
subnet_id = aws_subnet.public.id
tags = {
Name = "web-${count.index + 1}"
}
}Proxmox example:
resource "proxmox_vm_qemu" "web_server" {
name = "web-01"
target_node = "pve"
clone = "ubuntu-template"
cores = 2
memory = 2048
sockets = 1
disk {
size = "20G"
type = "scsi"
storage = "local-lvm"
}
network {
model = "virtio"
bridge = "vmbr0"
}
os_type = "cloud-init"
ipconfig0 = "ip=192.0.2.50/24,gw=192.0.2.1"
}4.3 Variables#
Variables parameterize configurations for reuse across environments.
Declaration (variables.tf):
variable "aws_region" {
description = "AWS region for resources"
type = string
default = "eu-central-1"
}
variable "environment" {
description = "Deployment environment"
type = string
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment must be dev, staging, or production."
}
}
variable "instance_count" {
description = "Number of web server instances"
type = number
default = 2
}
variable "allowed_cidrs" {
description = "List of CIDR blocks allowed to access the service"
type = list(string)
default = ["0.0.0.0/0"]
}
variable "instance_config" {
description = "Map of instance configuration"
type = map(object({
instance_type = string
ami_id = string
}))
}Values (terraform.tfvars):
aws_region = "eu-central-1"
environment = "production"
instance_count = 3
allowed_cidrs = ["10.0.0.0/8", "172.16.0.0/12"]
instance_config = {
web = {
instance_type = "t3.medium"
ami_id = "ami-0abcdef1234567890"
}
api = {
instance_type = "t3.large"
ami_id = "ami-0abcdef1234567890"
}
}Passing variables via CLI:
terraform apply -var="environment=staging" -var="instance_count=1"
terraform apply -var-file="production.tfvars"4.4 Outputs#
Outputs expose information about your infrastructure after apply, useful for passing data between modules or displaying results.
# outputs.tf
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "instance_public_ips" {
description = "Public IPs of web instances"
value = aws_instance.web[*].public_ip
}
output "database_endpoint" {
description = "Database connection endpoint"
value = aws_db_instance.main.endpoint
sensitive = true
}4.5 Data Sources#
Data sources fetch information from existing infrastructure that Terraform does not manage:
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
availability_zone = data.aws_availability_zones.available.names[0]
}4.6 Locals#
Locals define computed values for reuse within a module, reducing repetition:
locals {
common_tags = {
Environment = var.environment
ManagedBy = "terraform"
Project = var.project_name
}
name_prefix = "${var.project_name}-${var.environment}"
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web"
})
}5. Modules#
Modules are containers for multiple resources that are used together. They allow you to create reusable, composable infrastructure components.
Module definition (modules/webserver/main.tf):
variable "instance_type" {
type = string
default = "t3.micro"
}
variable "subnet_id" {
type = string
}
variable "tags" {
type = map(string)
default = {}
}
resource "aws_instance" "this" {
ami = var.ami_id
instance_type = var.instance_type
subnet_id = var.subnet_id
tags = var.tags
}
output "instance_id" {
value = aws_instance.this.id
}
output "private_ip" {
value = aws_instance.this.private_ip
}Using the module:
module "web_server" {
source = "./modules/webserver"
instance_type = "t3.medium"
subnet_id = aws_subnet.public.id
ami_id = data.aws_ami.ubuntu.id
tags = {
Name = "web-server"
Role = "frontend"
}
}
# Reference module outputs
output "web_ip" {
value = module.web_server.private_ip
}Using remote modules:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
azs = ["eu-central-1a", "eu-central-1b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
enable_nat_gateway = true
}6. State Management#
Terraform stores the state of managed infrastructure in a state file (terraform.tfstate). This file maps configuration to real-world resources and tracks metadata.
6.1 Backend Configuration#
By default, state is stored locally. For team use, configure a remote backend:
S3 backend (AWS):
# backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "infrastructure/terraform.tfstate"
region = "eu-central-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}HTTP backend (GitLab):
terraform {
backend "http" {
address = "https://gitlab.example.com/api/v4/projects/<id>/terraform/state/<name>"
lock_address = "https://gitlab.example.com/api/v4/projects/<id>/terraform/state/<name>/lock"
unlock_address = "https://gitlab.example.com/api/v4/projects/<id>/terraform/state/<name>/lock"
lock_method = "POST"
unlock_method = "DELETE"
retry_wait_min = 5
}
}Local backend (explicit):
terraform {
backend "local" {
path = "state/terraform.tfstate"
}
}6.2 State Best Practices#
- Never edit state files manually - use
terraform statecommands - Enable state locking - prevents concurrent modifications (DynamoDB for S3, built-in for Terraform Cloud)
- Encrypt state at rest - state files may contain sensitive values (passwords, IPs, keys)
- Use remote state - for any team or CI/CD workflow
- Separate state per environment - use workspaces or separate backends for dev/staging/production
- Back up state files - enable versioning on the S3 bucket or equivalent
7. Terraform Commands#
7.1 Format and Validate#
| Command | Description |
|---|---|
terraform fmt | Reformat configuration files to standard style |
terraform fmt -recursive | Format all .tf files in subdirectories |
terraform validate | Check whether the configuration is syntactically valid |
7.2 Initialize Working Directory#
| Command | Description |
|---|---|
terraform init | Download providers, initialize backend, prepare working directory |
terraform init -upgrade | Upgrade providers to latest allowed versions |
terraform init -reconfigure | Reconfigure backend, ignoring any saved configuration |
7.3 Plan, Deploy, and Cleanup#
| Command | Description |
|---|---|
terraform plan | Show execution plan without making changes |
terraform plan -out plan.out | Save plan to a file for later apply |
terraform plan -destroy | Show what would be destroyed |
terraform apply | Apply changes (prompts for confirmation) |
terraform apply --auto-approve | Apply without confirmation prompt |
terraform apply plan.out | Apply a saved plan file |
terraform apply -target=aws_instance.myinstance | Apply only to a specific resource |
terraform apply -var myregion=us-east-1 | Pass a variable via CLI |
terraform apply -lock=true | Lock the state file during apply |
terraform apply -refresh=false | Skip state refresh before planning |
terraform apply --parallelism=5 | Limit concurrent resource operations |
terraform destroy | Destroy all managed resources (prompts for confirmation) |
terraform destroy --auto-approve | Destroy without confirmation |
terraform refresh | Reconcile state with real-world resources |
terraform providers | Show providers used in the configuration |
7.4 Workspaces#
| Command | Description |
|---|---|
terraform workspace new <name> | Create a new workspace |
terraform workspace select <name> | Switch to a workspace |
terraform workspace list | List all workspaces |
terraform workspace show | Show the current workspace |
terraform workspace delete <name> | Delete a workspace |
Workspaces maintain separate state files within the same configuration, useful for managing multiple environments:
resource "aws_instance" "web" {
instance_type = terraform.workspace == "production" ? "t3.large" : "t3.micro"
tags = {
Environment = terraform.workspace
}
}7.5 State Manipulation#
| Command | Description |
|---|---|
terraform state list | List all resources in the state |
terraform state show <resource> | Show details of a specific resource |
terraform state pull > terraform.tfstate | Download remote state to a local file |
terraform state mv <source> <destination> | Move a resource in state (rename or move to module) |
terraform state rm <resource> | Remove a resource from state (stop managing it) |
terraform state replace-provider <old> <new> | Replace a provider in the state |
7.6 Import and Outputs#
| Command | Description |
|---|---|
terraform import <resource> <id> | Import an existing resource into Terraform state |
terraform output | List all output values |
terraform output <name> | Show a specific output value |
terraform output -json | List all outputs in JSON format |
Import example:
# Import an existing AWS instance
terraform import aws_instance.web i-1234567890abcdef0
# Import a Proxmox VM
terraform import proxmox_vm_qemu.web_server pve/qemu/1017.7 Terraform Cloud#
| Command | Description |
|---|---|
terraform login | Authenticate with Terraform Cloud using an API token |
terraform logout | Remove stored Terraform Cloud credentials |
8. CI/CD Integration#
Terraform integrates with CI/CD pipelines to automate infrastructure changes with review and approval workflows.
GitLab CI Example#
# .gitlab-ci.yml
stages:
- validate
- plan
- apply
variables:
TF_ROOT: ${CI_PROJECT_DIR}/infrastructure
validate:
stage: validate
script:
- cd ${TF_ROOT}
- terraform init -backend=false
- terraform fmt -check -recursive
- terraform validate
plan:
stage: plan
script:
- cd ${TF_ROOT}
- terraform init
- terraform plan -out=plan.out
artifacts:
paths:
- ${TF_ROOT}/plan.out
apply:
stage: apply
script:
- cd ${TF_ROOT}
- terraform init
- terraform apply plan.out
dependencies:
- plan
when: manual
only:
- mainGitHub Actions Example#
# .github/workflows/terraform.yml
name: Terraform
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Terraform Init
run: terraform init
- name: Terraform Format
run: terraform fmt -check
- name: Terraform Plan
run: terraform plan -no-color
if: github.event_name == 'pull_request'
- name: Terraform Apply
run: terraform apply -auto-approve
if: github.ref == 'refs/heads/main' && github.event_name == 'push'Best Practices for CI/CD#
- Always run
terraform planon pull requests for review - Require manual approval for
terraform applyin production - Store state remotely with locking enabled
- Use service accounts with minimal required permissions
- Pin provider and Terraform versions
- Store sensitive variables in CI/CD secret stores, never in
.tfvarscommitted to git
Troubleshooting#
| Issue | Cause | Solution |
|---|---|---|
Error: provider not found | Provider not initialized | Run terraform init or terraform init -upgrade |
Error: state lock | Another process holds the state lock | Wait for it to finish; use terraform force-unlock <id> as last resort |
Error: resource already exists | Resource exists but is not in state | Import it with terraform import |
| Drift detected on every plan | External changes to managed resources | Run terraform refresh; align manual changes with config |
Error: Cycle in plan | Circular dependency between resources | Refactor resource references; use depends_on explicitly |
| Sensitive values in state | State file contains secrets in plaintext | Enable backend encryption; restrict state access |
| Slow plan/apply | Too many resources in one state | Split into smaller modules with separate state files |
Error: Unsupported Terraform Core version | Version constraint mismatch | Update Terraform or adjust required_version |