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#

  1. Overview
  2. Core Concepts
  3. Setting Up Terraform
  4. HCL Configuration Language
  5. Modules
  6. State Management
  7. Terraform Commands
  8. CI/CD Integration
  9. Troubleshooting
  10. 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:

  1. Write - define infrastructure in .tf configuration files using HCL
  2. Plan - run terraform plan to preview changes (what will be created, modified, or destroyed)
  3. Apply - run terraform apply to execute the changes and update the state
Write .tf files -> terraform init -> terraform plan -> terraform apply
                                          |                   |
                                   Review changes      State updated

3. 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 --version

On Arch Linux:

sudo pacman -S terraform

On macOS (Homebrew):

brew install terraform

3.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.tf

4. 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 state commands
  • 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#

CommandDescription
terraform fmtReformat configuration files to standard style
terraform fmt -recursiveFormat all .tf files in subdirectories
terraform validateCheck whether the configuration is syntactically valid

7.2 Initialize Working Directory#

CommandDescription
terraform initDownload providers, initialize backend, prepare working directory
terraform init -upgradeUpgrade providers to latest allowed versions
terraform init -reconfigureReconfigure backend, ignoring any saved configuration

7.3 Plan, Deploy, and Cleanup#

CommandDescription
terraform planShow execution plan without making changes
terraform plan -out plan.outSave plan to a file for later apply
terraform plan -destroyShow what would be destroyed
terraform applyApply changes (prompts for confirmation)
terraform apply --auto-approveApply without confirmation prompt
terraform apply plan.outApply a saved plan file
terraform apply -target=aws_instance.myinstanceApply only to a specific resource
terraform apply -var myregion=us-east-1Pass a variable via CLI
terraform apply -lock=trueLock the state file during apply
terraform apply -refresh=falseSkip state refresh before planning
terraform apply --parallelism=5Limit concurrent resource operations
terraform destroyDestroy all managed resources (prompts for confirmation)
terraform destroy --auto-approveDestroy without confirmation
terraform refreshReconcile state with real-world resources
terraform providersShow providers used in the configuration

7.4 Workspaces#

CommandDescription
terraform workspace new <name>Create a new workspace
terraform workspace select <name>Switch to a workspace
terraform workspace listList all workspaces
terraform workspace showShow 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#

CommandDescription
terraform state listList all resources in the state
terraform state show <resource>Show details of a specific resource
terraform state pull > terraform.tfstateDownload 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#

CommandDescription
terraform import <resource> <id>Import an existing resource into Terraform state
terraform outputList all output values
terraform output <name>Show a specific output value
terraform output -jsonList 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/101

7.7 Terraform Cloud#

CommandDescription
terraform loginAuthenticate with Terraform Cloud using an API token
terraform logoutRemove 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:
    - main

GitHub 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 plan on pull requests for review
  • Require manual approval for terraform apply in 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 .tfvars committed to git

Troubleshooting#

IssueCauseSolution
Error: provider not foundProvider not initializedRun terraform init or terraform init -upgrade
Error: state lockAnother process holds the state lockWait for it to finish; use terraform force-unlock <id> as last resort
Error: resource already existsResource exists but is not in stateImport it with terraform import
Drift detected on every planExternal changes to managed resourcesRun terraform refresh; align manual changes with config
Error: Cycle in planCircular dependency between resourcesRefactor resource references; use depends_on explicitly
Sensitive values in stateState file contains secrets in plaintextEnable backend encryption; restrict state access
Slow plan/applyToo many resources in one stateSplit into smaller modules with separate state files
Error: Unsupported Terraform Core versionVersion constraint mismatchUpdate Terraform or adjust required_version

Sources#