Ansible configures servers. Terraform creates them.
That’s the simplest distinction. Terraform is infrastructure as code for provisioning – it creates the VMs, networks, storage accounts, and cloud resources that Ansible then configures. While Ansible can provision cloud resources, Terraform is purpose-built for it.
If you’re working with any cloud platform (AWS, Azure, GCP) or modern on-premises infrastructure (vSphere, Proxmox), Terraform is the tool hiring managers expect you to know.
Career Impact
Terraform is the lingua franca of cloud infrastructure.
Every serious cloud team uses Infrastructure as Code, and Terraform is the most widely used multi-cloud IaC tool. It turns up constantly in Cloud Engineer, DevOps and Platform Engineer job descriptions, and the skills carry straight over to its open-source fork, OpenTofu.

What You’ll Learn
- What Terraform is and when to use it
- HCL basics (HashiCorp Configuration Language)
- Providers, resources, and state
- Writing your first Terraform configuration
- Essential commands and workflows
Quick Reference
| Concept | What It Is | Example |
|---|---|---|
| Provider | Plugin for a platform | azurerm, aws, vsphere |
| Resource | Infrastructure object | VM, network, storage account |
| Data Source | Existing infrastructure | Look up existing subnet |
| State | Terraform’s record of what exists | terraform.tfstate |
| Plan | Preview of changes | terraform plan |
| Apply | Execute changes | terraform apply |
| Module | Reusable configuration package | modules/webserver |
Why Terraform?
The Problem It Solves
Without IaC:
- Click through cloud portals
- No record of what you created
- Can’t recreate environments
- Manual, error-prone, slow
With Terraform:
- Define infrastructure in code
- Version control your infrastructure
- Review changes before applying
- Destroy and recreate identically
- Collaborate with team
Terraform vs Cloud-Native Tools
| Feature | Terraform | ARM/CloudFormation/Bicep |
|---|---|---|
| Multi-cloud | Yes | No (vendor-specific) |
| Language | HCL | JSON/YAML/DSL |
| State management | Required | Handled by cloud |
| Learning once | Works everywhere | Learn each platform |
| Drift detection | Built-in | Varies |
Terraform’s multi-cloud capability is its killer feature. Learn it once, use it on AWS, Azure, GCP, VMware, and more.
Installation
Download and Install
Install from HashiCorp’s own package repository rather than a hard-coded zip, so apt upgrade keeps you current. At the time of writing (October 2026) the current release is Terraform 1.16. The repository covers Ubuntu 24.04 and 26.04 and Debian 12 and 13.
# Ubuntu / Debian (HashiCorp apt repository)
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
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
sudo apt update && sudo apt install terraform
# macOS (Homebrew: use HashiCorp's tap; the core formula stopped at 1.5.7)
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
# Verify
terraform version
# Windows (Chocolatey)
choco install terraform
Terraform or OpenTofu? HashiCorp moved Terraform from the open-source MPL licence to the Business Source Licence (BSL) from version 1.6 in 2023. Using it to manage your own infrastructure is unaffected; the licence restricts building a competing commercial product. The Linux Foundation’s OpenTofu is the open-source fork: same HCL, same providers, and the commands are the same with tofu in place of terraform. Everything on this page works with either.
HCL Basics
The Language
Terraform uses HCL (HashiCorp Configuration Language). It’s designed to be human-readable:
# This is a comment
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "WebServer"
}
}
Blocks
Everything in Terraform is a block:
block_type "label1" "label2" {
argument1 = "value"
argument2 = 123
nested_block {
nested_argument = "nested_value"
}
}
Data Types
# Strings
name = "webserver"
# Numbers
count = 3
# Booleans
enabled = true
# Lists
subnets = ["subnet-1", "subnet-2"]
# Maps
tags = {
environment = "production"
owner = "ops-team"
}
Your First Configuration
Project Structure
terraform-demo/
├── main.tf # Main configuration
├── variables.tf # Input variables
├── outputs.tf # Output values
└── terraform.tfvars # Variable values
Simple Azure Example
main.tf:
# Configure the Azure provider
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 5.0"
}
}
}
# azurerm 4.0+ needs a subscription ID for plan/apply: set it here
# or export ARM_SUBSCRIPTION_ID. From 5.0 the provider no longer
# registers Azure resource providers for you, so list the ones you use.
provider "azurerm" {
subscription_id = var.subscription_id
resource_providers_to_register = [
"Microsoft.Compute",
"Microsoft.Network",
]
features {}
}
# Create a resource group
resource "azurerm_resource_group" "main" {
name = var.resource_group_name
location = var.location
tags = {
environment = var.environment
}
}
# Create a virtual network
resource "azurerm_virtual_network" "main" {
name = "${var.prefix}-vnet"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
}
# Create a subnet
resource "azurerm_subnet" "main" {
name = "${var.prefix}-subnet"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.1.0/24"]
}
variables.tf:
variable "subscription_id" {
description = "Azure subscription ID (az account show --query id -o tsv)"
type = string
}
variable "resource_group_name" {
description = "Name of the resource group"
type = string
default = "demo-rg"
}
variable "location" {
description = "Azure region"
type = string
default = "UK South"
}
variable "prefix" {
description = "Prefix for resource names"
type = string
default = "demo"
}
variable "environment" {
description = "Environment tag"
type = string
default = "development"
}
outputs.tf:
output "resource_group_id" {
description = "ID of the created resource group"
value = azurerm_resource_group.main.id
}
output "vnet_id" {
description = "ID of the virtual network"
value = azurerm_virtual_network.main.id
}
output "subnet_id" {
description = "ID of the subnet"
value = azurerm_subnet.main.id
}
Essential Commands
The Workflow
# 1. Initialize (download providers)
terraform init
# 2. Format code
terraform fmt
# 3. Validate syntax
terraform validate
# 4. Plan (preview changes)
terraform plan
# 5. Apply (make changes)
terraform apply
# 6. Destroy (remove everything)
terraform destroy
terraform init
Downloads providers and sets up the working directory:
$ terraform init
Initializing provider plugins...
- Finding hashicorp/azurerm versions matching "~> 5.0"...
- Installing hashicorp/azurerm v5.7.0...
Terraform has been successfully initialized!
terraform plan
Shows what Terraform will do without making changes:
$ terraform plan
Terraform will perform the following actions:
# azurerm_resource_group.main will be created
+ resource "azurerm_resource_group" "main" {
+ id = (known after apply)
+ location = "uksouth"
+ name = "demo-rg"
+ tags = {
+ "environment" = "development"
}
}
Plan: 3 to add, 0 to change, 0 to destroy.
terraform apply
Executes the plan:
$ terraform apply
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
azurerm_resource_group.main: Creating...
azurerm_resource_group.main: Creation complete after 2s [id=/subscriptions/xxx/resourceGroups/demo-rg]
State Management
What is State?
Terraform tracks what it created in a state file (terraform.tfstate). This is critical:
- Maps configuration to real resources
- Tracks dependencies
- Enables change detection
- Contains sensitive data
Local vs Remote State
Local (default): State in local terraform.tfstate
- Fine for learning
- Dangerous for teams
- No locking
Remote (production): State in shared storage
- Team collaboration
- State locking
- Backup and history
Azure Backend Example:
terraform {
backend "azurerm" {
resource_group_name = "terraform-state-rg"
storage_account_name = "tfstateaccount"
container_name = "tfstate"
key = "prod.terraform.tfstate"
}
}
State Commands
# List resources in state
terraform state list
# Show specific resource
terraform state show azurerm_resource_group.main
# Remove resource from state (doesn't delete it)
terraform state rm azurerm_resource_group.main
# Import existing resource into state
terraform import azurerm_resource_group.main /subscriptions/xxx/resourceGroups/demo-rg
Variables and Outputs
Variable Types
# String
variable "name" {
type = string
default = "myapp"
}
# Number
variable "instance_count" {
type = number
default = 2
}
# Boolean
variable "enable_monitoring" {
type = bool
default = true
}
# List
variable "availability_zones" {
type = list(string)
default = ["1", "2", "3"]
}
# Map
variable "tags" {
type = map(string)
default = {
environment = "dev"
team = "platform"
}
}
# Object
variable "vm_config" {
type = object({
size = string
os_disk = number
})
default = {
size = "Standard_B2s"
os_disk = 128
}
}
Setting Variables
terraform.tfvars:
resource_group_name = "production-rg"
location = "UK South"
environment = "production"
Command line:
terraform apply -var="environment=staging"
Environment variables:
export TF_VAR_environment="staging"
terraform apply
Using Outputs
Outputs expose values after apply:
output "vm_ip" {
value = azurerm_public_ip.main.ip_address
}
Use in scripts:
VM_IP=$(terraform output -raw vm_ip)
ssh admin@$VM_IP
Data Sources
Look up existing resources:
# Find existing resource group
data "azurerm_resource_group" "existing" {
name = "existing-rg"
}
# Use it in new resource
resource "azurerm_virtual_network" "main" {
name = "my-vnet"
location = data.azurerm_resource_group.existing.location
resource_group_name = data.azurerm_resource_group.existing.name
address_space = ["10.0.0.0/16"]
}
Practical Examples
Example 1: Complete VM Deployment
# Variables
variable "prefix" {
default = "demo"
}
variable "admin_password" {
description = "Admin password for VM"
type = string
sensitive = true
}
# Resource Group
resource "azurerm_resource_group" "main" {
name = "${var.prefix}-rg"
location = "UK South"
}
# Network
resource "azurerm_virtual_network" "main" {
name = "${var.prefix}-vnet"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
}
resource "azurerm_subnet" "main" {
name = "${var.prefix}-subnet"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.1.0/24"]
}
# Public IP
resource "azurerm_public_ip" "main" {
name = "${var.prefix}-pip"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
allocation_method = "Static"
}
# Network Interface
resource "azurerm_network_interface" "main" {
name = "${var.prefix}-nic"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.main.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.main.id
}
}
# Virtual Machine
resource "azurerm_linux_virtual_machine" "main" {
name = "${var.prefix}-vm"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
size = "Standard_B2s"
admin_username = "adminuser"
admin_password = var.admin_password
disable_password_authentication = false
network_interface_ids = [
azurerm_network_interface.main.id,
]
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
}
# Outputs
output "public_ip" {
value = azurerm_public_ip.main.ip_address
}
Example 2: Using Modules
modules/webserver/main.tf:
variable "name" {}
variable "resource_group_name" {}
variable "location" {}
variable "subnet_id" {}
resource "azurerm_network_interface" "main" {
name = "${var.name}-nic"
location = var.location
resource_group_name = var.resource_group_name
ip_configuration {
name = "internal"
subnet_id = var.subnet_id
private_ip_address_allocation = "Dynamic"
}
}
# ... rest of VM configuration
main.tf (using module):
module "webserver1" {
source = "./modules/webserver"
name = "web01"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
subnet_id = azurerm_subnet.main.id
}
module "webserver2" {
source = "./modules/webserver"
name = "web02"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
subnet_id = azurerm_subnet.main.id
}
Best Practices
Code Organization
- Use modules – Reusable, testable components
- Separate environments – Different state for prod/staging
- Consistent naming –
{project}-{environment}-{resource} - Version control – Everything except state and secrets
Security
- Never commit state – Add to
.gitignore - Use remote state – With encryption and locking
- Sensitive variables – Mark as
sensitive = true - Least privilege – Service principals with minimal permissions
Operations
- Always plan first – Review before apply
- Use workspaces – For environment separation
- Lock state – Prevent concurrent modifications
- Backup state – It’s your source of truth
.gitignore for Terraform
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
crash.*.log
# Exclude all .tfvars files (may contain secrets)
*.tfvars
*.tfvars.json
# Override files
override.tf
override.tf.json
*_override.tf
*_override.tf.json
Interview Questions
Q1: “Explain the difference between Terraform and Ansible.”
Good Answer: “They solve different problems. Terraform is for provisioning – creating infrastructure like VMs, networks, and cloud services. It tracks state and manages the lifecycle of resources. Ansible is for configuration – installing software, deploying applications, managing settings on existing servers. In practice, you use both: Terraform creates the servers, Ansible configures them. Terraform is declarative and idempotent for infrastructure state, while Ansible is procedural but also idempotent for configuration tasks.”
Q2: “What is Terraform state and why is it important?”
Good Answer: “State is Terraform’s record of what it has created. It maps your configuration to real resources in the cloud. Without state, Terraform wouldn’t know what exists – it would try to create everything fresh every time. State is critical for detecting drift, planning changes, and managing dependencies. For teams, remote state with locking is essential to prevent conflicts. State contains sensitive data, so it must be secured – encrypted in storage, access controlled.”
Q3: “How would you manage Terraform for multiple environments?”
Good Answer: “Several approaches work. I prefer separate state files per environment using different backend configurations or workspaces. Variables differ per environment using .tfvars files like production.tfvars and staging.tfvars. The code is the same – only variables change. For true isolation, separate directories with their own state. CI/CD pipelines then target specific environments with the appropriate variable files. The key is keeping prod state completely separate from non-prod.”
Career Application
On Your Resume
- “Managed cloud infrastructure for 50+ Azure resources using Terraform”
- “Implemented infrastructure as code reducing provisioning time from days to minutes”
- “Designed Terraform modules for standardized VM deployments across environments”
Demonstrate
- Multi-cloud awareness
- State management understanding
- Module design capability
- Security considerations
Next Steps
- Practice: Deploy something in Azure with Terraform, then destroy it
- Explore: Terraform Registry for pre-built modules
- Integrate: Add Terraform to CI/CD pipelines
- Advance: Learn HCP Terraform (formerly Terraform Cloud) for team collaboration
Infrastructure as code means your infrastructure is reproducible, reviewable, and version controlled. Start with one resource, build to entire environments.
Part 4 of 4
You’ve finished the Automation for Sysadmins series!
This guide is part of the Automation Fundamentals series. See the full series for more guides like this.
Frequently Asked Questions
What is Terraform and how is it different from Ansible?
Terraform is an infrastructure as code tool that creates and manages cloud resources (VMs, networks, storage) using declarative configuration files. Ansible configures existing servers. Terraform builds the infrastructure, Ansible configures what runs on it. They are complementary tools, not competitors.
Is Terraform only for cloud infrastructure?
No. While Terraform is most commonly used with AWS, Azure, and GCP, it has providers for hundreds of platforms including VMware, Proxmox, Cloudflare, GitHub, and Kubernetes. You can manage DNS records, firewall rules, database clusters, and even physical infrastructure through supported providers.
What programming language does Terraform use?
Terraform uses HCL (HashiCorp Configuration Language), a declarative language designed specifically for infrastructure definitions. HCL is not a general-purpose programming language. It uses blocks, arguments, and expressions to describe resources. It is intentionally simpler than languages like Python or Go to keep infrastructure definitions readable.
How does Terraform track what infrastructure it has created?
Terraform maintains a state file (terraform.tfstate) that maps your configuration to real-world resources. This state file records resource IDs, attributes, and dependencies. When you run terraform plan, it compares your configuration against the state to determine what needs to change. Never manually edit the state file.
Should I learn Terraform or CloudFormation for Azure?
Learn Terraform. CloudFormation is AWS-only. Terraform works across all major cloud providers with consistent syntax, so your skills transfer between AWS, Azure, and GCP. Azure also has Bicep (its native IaC tool), but Terraform is the de facto standard for multi-cloud work and the IaC tool you will see named most often in cloud job descriptions.
RTFM Apparel · printed in the UK

ReadTheManual is run, written and curated by Eric Lonsdale.
Eric has over 20 years of professional experience in IT infrastructure, cloud architecture, and cybersecurity, but started with PCs long before that.
He built his first machine from parts bought off tables at the local college campus, hoping they worked. He learned on BBC Micros and Atari units in the early 90s, and has built almost every PC he’s used between 1995 and now.
From helpdesk to infrastructure architect, Eric has worked across enterprise datacentres, Azure environments, and security operations. He’s managed teams, trained engineers, and spent two decades solving the problems this site teaches you to solve.
ReadTheManual exists because Eric believes the best way to learn IT is to build things, break things, and actually read the manual. Every guide on this site runs on infrastructure he owns and maintains.
Enjoyed this guide?
New articles on Linux, homelab, cloud, and automation every 2 days. No spam, unsubscribe anytime.







