Table of Contents

Terraform is an open-source Infrastructure as Code (IaC) tool that allows you to define, provision, and manage cloud infrastructure and services using declarative configuration files written in HashiCorp Configuration Language (HCL).

Note

Terraform enables you to safely and predictably create, change, and improve infrastructure. It's provider-agnostic and can manage both cloud and on-premises resources.

Key Features

  • Multi-provider support: AWS, Azure, Google Cloud, VMware, Proxmox, Kubernetes, and 3000+ providers
  • Declarative configuration: Define desired state rather than step-by-step procedures
  • Version control friendly: Human-readable HCL files work well with Git
  • Dependency management: Automatically handles resource creation order and dependencies
  • State management: Tracks infrastructure state and detects configuration drift
  • Plan and apply workflow: Preview changes before execution
  • Modular and reusable: Create and share infrastructure modules

Installation

Use the built-in package management system for your Linux distribution to install Terraform.

Ensure that your system is up to date and that you have installed the gnupg and software-properties-common packages. You will use these packages to verify HashiCorp's GPG signature and install HashiCorp's Debian package repository.

sudo apt-get update && sudo apt-get install -y gnupg software-properties-common

Install HashiCorp's GPG key.

wget -O- https://apt.releases.hashicorp.com/gpg | \
gpg --dearmor | \
sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null

Verify the GPG key's fingerprint.

gpg --no-default-keyring \
--keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg \
--fingerprint

The gpg command reports the key fingerprint:

/usr/share/keyrings/hashicorp-archive-keyring.gpg
-------------------------------------------------
pub   rsa4096 XXXX-XX-XX [SC]
AAAA AAAA AAAA AAAA
uid         [ unknown] HashiCorp Security (HashiCorp Package Signing) <security+packaging@hashicorp.com>
sub   rsa4096 XXXX-XX-XX [E]

Add the official HashiCorp repository to your system.

echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list

Update apt to download the package information from the HashiCorp repository.

sudo apt update

Install Terraform from the new repository.

sudo apt-get install terraform

Core Concepts

Configuration Files

Terraform configurations are written in HCL (HashiCorp Configuration Language) with .tf file extensions.

State File

Terraform maintains a state file (terraform.tfstate) that maps real-world resources to your configuration.

Providers

Providers are plugins that enable Terraform to interact with APIs of cloud providers and other services.

Resources

Resources are the most important element in Terraform - they describe infrastructure objects like virtual machines, DNS records, or databases.

Essential Workflow

Tip

Always run terraform plan before terraform apply to review changes and avoid unintended modifications to your infrastructure.

1. Initialize Working Directory

terraform init

This downloads required providers and modules, initializes the backend, and sets up the working directory.

2. Create Execution Plan

terraform plan

Shows what actions Terraform will take to achieve the desired state defined in your configuration.

3. Apply Configuration

terraform apply

Executes the planned changes to create, modify, or destroy infrastructure resources.

4. Destroy Infrastructure

terraform destroy

Removes all resources defined in your Terraform configuration.

Basic Configuration Example

Here's a simple Azure Virtual Machine configuration:

# Configure the Azure Provider
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}

# Configure the Azure Provider features
provider "azurerm" {
  features {}
}

# Create a resource group
resource "azurerm_resource_group" "example" {
  name     = "rg-example"
  location = "East US"
}

# Create a virtual network
resource "azurerm_virtual_network" "example" {
  name                = "vnet-example"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
}

# Create a subnet
resource "azurerm_subnet" "example" {
  name                 = "subnet-example"
  resource_group_name  = azurerm_resource_group.example.name
  virtual_network_name = azurerm_virtual_network.example.name
  address_prefixes     = ["10.0.1.0/24"]
}

# Create a virtual machine
resource "azurerm_linux_virtual_machine" "example" {
  name                = "vm-example"
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
  size                = "Standard_B1s"
  admin_username      = "adminuser"
  
  network_interface_ids = [
    azurerm_network_interface.example.id,
  ]

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-focal"
    sku       = "20_04-lts-gen2"
    version   = "latest"
  }
  
  tags = {
    Environment = "Development"
  }
}

Best Practices

Important

Following these best practices will help you maintain secure, scalable, and maintainable Terraform configurations.

Security and State Management

  • Use remote state storage: Store state files in remote backends (Azure Storage, AWS S3, etc.)
  • Enable state locking: Prevent concurrent modifications using Azure Storage or similar
  • Never commit sensitive data: Use .gitignore to exclude .tfstate, .tfvars, and sensitive files
  • Use environment variables: Store secrets in environment variables, not in configuration files

Code Organization

  • Use modules: Create reusable infrastructure components
  • Follow naming conventions: Use consistent, descriptive names for resources
  • Structure your code: Organize files logically (main.tf, variables.tf, outputs.tf)
  • Version your modules: Tag and version your modules for stability

Development Workflow

  • Use workspaces: Manage multiple environments (dev, staging, prod)
  • Run terraform fmt: Format your code consistently
  • Validate configurations: Use terraform validate to check syntax
  • Use terraform plan: Always preview changes before applying

Example Directory Structure

terraform/
├── modules/
│   ├── network/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   └── vm/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   └── prod/
│       ├── main.tf
│       ├── variables.tf
│       └── terraform.tfvars
└── .gitignore

Common Commands

Essential Commands

# Initialize a working directory
terraform init

# Format code to canonical format
terraform fmt

# Validate configuration files
terraform validate

# Show current state
terraform show

# List resources in state
terraform state list

# Import existing infrastructure
terraform import azurerm_resource_group.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/example-rg

# Refresh state against real infrastructure
terraform refresh

# Create a plan and save it
terraform plan -out=tfplan

# Apply a saved plan
terraform apply tfplan

Working with State

# Show resources in state
terraform state show azurerm_resource_group.example

# Move resource in state
terraform state mv azurerm_resource_group.example azurerm_resource_group.new_name

# Remove resource from state (without destroying)
terraform state rm azurerm_resource_group.example

# Pull remote state to local
terraform state pull

# Push local state to remote
terraform state push terraform.tfstate

Variables and Outputs

Variable Types

# String variable
variable "vm_size" {
  description = "Azure VM size"
  type        = string
  default     = "Standard_B1s"
}

# Number variable
variable "vm_count" {
  description = "Number of VMs"
  type        = number
  default     = 1
}

# Boolean variable
variable "enable_monitoring" {
  description = "Enable Azure Monitor"
  type        = bool
  default     = false
}

# List variable
variable "allowed_subnets" {
  description = "List of allowed subnet CIDRs"
  type        = list(string)
  default     = ["10.0.1.0/24", "10.0.2.0/24"]
}

# Object variable
variable "vm_config" {
  description = "VM configuration"
  type = object({
    vm_size    = string
    image_sku  = string
    admin_user = string
  })
  default = {
    vm_size    = "Standard_B1s"
    image_sku  = "20_04-lts-gen2"
    admin_user = "adminuser"
  }
}

Outputs

# Output values
output "resource_group_id" {
  description = "ID of the resource group"
  value       = azurerm_resource_group.example.id
}

output "vm_public_ip" {
  description = "Public IP address of the VM"
  value       = azurerm_public_ip.example.ip_address
  sensitive   = true
}

output "vm_fqdn" {
  description = "Fully qualified domain name of the VM"
  value       = azurerm_public_ip.example.fqdn
}

Advanced Features

Conditional Resources

# Create resource only if condition is met
resource "azurerm_linux_virtual_machine" "conditional" {
  count = var.create_vm ? 1 : 0
  
  name                = "vm-conditional-${count.index}"
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
  size                = var.vm_size
  admin_username      = var.admin_username
  
  network_interface_ids = [
    azurerm_network_interface.example[count.index].id,
  ]

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-focal"
    sku       = var.vm_config.image_sku
    version   = "latest"
  }
}

Dynamic Blocks

# Dynamic network security group rules
resource "azurerm_network_security_group" "example" {
  name                = "nsg-example"
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
  
  dynamic "security_rule" {
    for_each = var.security_rules
    content {
      name                       = security_rule.value.name
      priority                   = security_rule.value.priority
      direction                  = security_rule.value.direction
      access                     = security_rule.value.access
      protocol                   = security_rule.value.protocol
      source_port_range          = security_rule.value.source_port_range
      destination_port_range     = security_rule.value.destination_port_range
      source_address_prefix      = security_rule.value.source_address_prefix
      destination_address_prefix = security_rule.value.destination_address_prefix
    }
  }
}

Data Sources

# Get existing resource group
data "azurerm_resource_group" "existing" {
  name = "existing-rg"
}

# Get latest Ubuntu image
data "azurerm_platform_image" "ubuntu" {
  location  = "East US"
  publisher = "Canonical"
  offer     = "0001-com-ubuntu-server-focal"
  sku       = "20_04-lts-gen2"
}

# Get client configuration
data "azurerm_client_config" "current" {}

Troubleshooting

Common Issues

Warning

Always backup your state file before performing manual state operations or major changes.

  • State lock errors: Use terraform force-unlock LOCK_ID to release stuck locks
  • Resource conflicts: Check for naming conflicts or resource limits
  • Provider version issues: Pin provider versions in your configuration
  • Authentication errors: Verify credentials and permissions

Debugging

# Enable verbose logging
export TF_LOG=DEBUG
export TF_LOG_PATH=terraform.log

# Run with detailed output
terraform plan -detailed-exitcode

# Validate with detailed errors
terraform validate -json

# Debug Azure authentication
az account show
az account list

Next Steps