A comprehensive guide to learning and using Terraform with Microsoft Azure - the Infrastructure as Code (IaC) tool for Azure cloud.
- What is Terraform?
- Why Use Terraform with Azure?
- Installation
- Azure Setup
- Getting Started
- Core Concepts
- Basic Commands
- Configuration Structure
- Examples
- Best Practices
- Common Patterns
- Troubleshooting
- Resources
Terraform is an open-source Infrastructure as Code (IaC) tool created by HashiCorp. It allows you to define and provision infrastructure using a declarative configuration language called HashiCorp Configuration Language (HCL).
With Terraform and Azure, you can:
- Manage Azure infrastructure as code
- Version control your Azure infrastructure
- Automate Azure resource provisioning
- Create reusable Azure infrastructure components
- Track changes and maintain state of Azure resources
- Infrastructure as Code: Define Azure resources in version-controlled configuration files
- Declarative: Describe what Azure resources you want, not how to create them
- Version Control: Track Azure infrastructure changes in Git
- Reusable: Create modules for repeatable Azure infrastructure patterns
- Planning: Preview Azure resource changes before applying them
- State Management: Keep track of your Azure infrastructure state
- Azure Integration: Native support for Azure services through the AzureRM provider
- Multi-Environment: Easily manage dev, staging, and production environments
brew tap hashicorp/tap
brew install hashicorp/tap/terraformwget -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 terraformchoco install terraformterraform versionBefore you can use Terraform with Azure, you need to set up authentication.
-
Install Azure CLI:
- macOS:
brew install azure-cli - Linux: Follow official docs
- Windows: Download from Microsoft
- macOS:
-
Login to Azure:
az login
-
Set your subscription (if you have multiple):
az account list --output table az account set --subscription "YOUR_SUBSCRIPTION_ID"
-
Create a Service Principal:
az ad sp create-for-rbac --name "terraform-sp" --role="Contributor" --scopes="/subscriptions/YOUR_SUBSCRIPTION_ID"
-
Set environment variables:
export ARM_CLIENT_ID="<appId>" export ARM_CLIENT_SECRET="<password>" export ARM_SUBSCRIPTION_ID="<subscription_id>" export ARM_TENANT_ID="<tenant>"
az account showCreate a file named main.tf:
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "example" {
name = "rg-terraform-demo"
location = "East US"
}terraform initThis downloads the required providers.
terraform planPreview what Azure resources Terraform will create.
terraform applyType yes to confirm and create the Azure resources.
az group show --name rg-terraform-demoterraform destroyRemove all Azure resources created by Terraform.
Providers are plugins that interact with APIs of cloud providers and services.
provider "azurerm" {
features {}
subscription_id = "00000000-0000-0000-0000-000000000000"
}Resources are the most important element in Terraform. They represent infrastructure objects.
resource "azurerm_virtual_machine" "web" {
name = "web-vm"
location = "East US"
resource_group_name = azurerm_resource_group.main.name
vm_size = "Standard_B2s"
}Variables allow you to parameterize your configurations.
variable "vm_size" {
description = "Azure VM size"
type = string
default = "Standard_B2s"
}Outputs display information about your infrastructure.
output "vm_ip" {
value = azurerm_public_ip.main.ip_address
}Data sources allow you to fetch information from existing resources.
data "azurerm_image" "ubuntu" {
name = "ubuntu-20-04"
resource_group_name = "images-rg"
}Modules are containers for multiple resources that are used together.
module "network" {
source = "./modules/network"
address_space = "10.0.0.0/16"
name = "my-vnet"
}Terraform stores the state of your infrastructure in a state file (terraform.tfstate). This file maps your configuration to real-world resources.
| Command | Description |
|---|---|
terraform init |
Initialize a Terraform working directory |
terraform plan |
Preview changes before applying |
terraform apply |
Create or update infrastructure |
terraform destroy |
Destroy all managed infrastructure |
terraform fmt |
Format configuration files |
terraform validate |
Check configuration syntax |
terraform show |
Show current state or plan |
terraform output |
Display output values |
terraform state list |
List resources in state |
terraform state show <resource> |
Show details of a resource |
terraform refresh |
Update state to match remote resources |
terraform import |
Import existing infrastructure |
terraform workspace list |
List workspaces |
terraform workspace new <name> |
Create a new workspace |
terraform workspace select <name> |
Switch to a workspace |
A typical Terraform project structure:
.
├── main.tf # Main configuration
├── variables.tf # Variable definitions
├── outputs.tf # Output definitions
├── providers.tf # Provider configurations
├── terraform.tfvars # Variable values (not committed to git)
├── versions.tf # Required Terraform and provider versions
├── modules/ # Reusable modules
│ └── vnet/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── environments/ # Environment-specific configs
├── dev/
├── staging/
└── prod/
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}
provider "azurerm" {
features {}
}
variable "location" {
default = "East US"
}
resource "azurerm_resource_group" "example" {
name = "rg-example"
location = var.location
}
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
}
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"]
}
resource "azurerm_network_interface" "example" {
name = "nic-example"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.example.id
private_ip_address_allocation = "Dynamic"
}
}
resource "azurerm_linux_virtual_machine" "example" {
name = "vm-example"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
size = "Standard_B2s"
admin_username = "adminuser"
network_interface_ids = [
azurerm_network_interface.example.id,
]
admin_ssh_key {
username = "adminuser"
public_key = file("~/.ssh/id_rsa.pub")
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-focal"
sku = "20_04-lts"
version = "latest"
}
tags = {
Name = "ExampleVM"
}
}
output "vm_id" {
value = azurerm_linux_virtual_machine.example.id
}
output "private_ip" {
value = azurerm_network_interface.example.private_ip_address
}Note: This example uses the
randomprovider. Include it in yourrequired_providersblock:terraform { required_providers { azurerm = { source = "hashicorp/azurerm" version = "~> 3.0" } random = { source = "hashicorp/random" version = "~> 3.0" } } }
resource "azurerm_resource_group" "storage" {
name = "rg-storage"
location = "East US"
}
resource "azurerm_storage_account" "example" {
name = "stexample${random_id.storage_id.hex}"
resource_group_name = azurerm_resource_group.storage.name
location = azurerm_resource_group.storage.location
account_tier = "Standard"
account_replication_type = "LRS"
blob_properties {
versioning_enabled = true
}
tags = {
Name = "StorageAccount"
Environment = "Dev"
}
}
resource "random_id" "storage_id" {
byte_length = 4
}
output "storage_account_name" {
value = azurerm_storage_account.example.name
}variable "environment" {
description = "Environment name"
type = string
}
variable "vm_count" {
description = "Number of VMs"
type = number
default = 1
}
locals {
common_tags = {
Environment = var.environment
ManagedBy = "Terraform"
}
}
resource "azurerm_linux_virtual_machine" "app" {
count = var.vm_count
name = "vm-app-${count.index + 1}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
size = "Standard_B2s"
tags = merge(
local.common_tags,
{
Name = "app-${count.index + 1}"
}
)
}- Always commit your
.tffiles to version control - Never commit
terraform.tfstateor.tfvarsfiles with secrets - Use
.gitignorefor sensitive files
Store state remotely for team collaboration using Azure Storage:
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "sttfstate"
container_name = "tfstate"
key = "prod.terraform.tfstate"
}
}- Parameterize everything that might change
- Provide sensible defaults
- Document variables with descriptions
- Split configurations into multiple files
- Use modules for reusable components
- Follow a consistent naming convention
terraform fmt -recursiveterraform validate
terraform planterraform workspace new dev
terraform workspace new staging
terraform workspace new prodAlways tag resources for better organization and cost tracking:
tags = {
Environment = "production"
Project = "myapp"
ManagedBy = "terraform"
}Leverage data sources instead of hardcoding values:
data "azurerm_client_config" "current" {}
data "azurerm_virtual_network" "existing" {
name = "existing-vnet"
resource_group_name = "existing-rg"
}Use state locking to prevent concurrent modifications:
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "sttfstate"
container_name = "tfstate"
key = "terraform.tfstate"
}
}Note: Azure Storage backend automatically supports state locking.
Using count:
resource "azurerm_linux_virtual_machine" "server" {
count = 3
name = "vm-server-${count.index}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
size = "Standard_B2s"
tags = {
Name = "server-${count.index}"
}
}Using for_each (preferred for flexibility):
variable "servers" {
type = map(object({
vm_size = string
}))
default = {
web = { vm_size = "Standard_B2s" }
app = { vm_size = "Standard_B4ms" }
}
}
resource "azurerm_linux_virtual_machine" "server" {
for_each = var.servers
name = "vm-${each.key}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
size = each.value.vm_size
tags = {
Name = each.key
}
}resource "azurerm_network_security_group" "example" {
name = "nsg-example"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.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
}
}
}resource "azurerm_linux_virtual_machine" "example" {
count = var.create_vm ? 1 : 0
name = "vm-example"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
size = "Standard_B2s"
}resource "azurerm_linux_virtual_machine" "web" {
name = "vm-web"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
size = "Standard_B2s"
depends_on = [azurerm_network_security_group.web]
}Issue: Provider plugin not found
terraform initIssue: State lock errors
# Force unlock (use carefully)
terraform force-unlock <LOCK_ID>Issue: State drift
terraform refresh
terraform planIssue: Resource already exists
# Import existing Azure resource
terraform import azurerm_resource_group.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroupIssue: Configuration syntax errors
terraform validate
terraform fmtEnable detailed logging:
export TF_LOG=DEBUG
terraform applyLog levels: TRACE, DEBUG, INFO, WARN, ERROR
Save logs to a file:
export TF_LOG=DEBUG
export TF_LOG_PATH=./terraform.log
terraform apply- Terraform Documentation
- Azure Provider Documentation
- Terraform Registry - Providers and modules
- HashiCorp Learn - Azure - Interactive tutorials
- Azure Terraform QuickStart Templates
- Azure CAF Terraform Landing Zones
- Azure Verified Modules
- Microsoft Learn - Terraform on Azure
- "Terraform: Up and Running" by Yevgeniy Brikman
- "Terraform in Action" by Scott Winkler
- HashiCorp Certified: Terraform Associate Certification
- Microsoft Learn: Infrastructure as Code with Terraform
- tflint - Terraform linter with Azure rules
- terraform-docs - Generate documentation
- terragrunt - Terraform wrapper
- checkov - Static code analysis for IaC (Azure support)
- infracost - Azure cloud cost estimates
- terraform-compliance - BDD testing for Terraform
Contributions are welcome! Please feel free to submit a Pull Request.
This repository is for educational purposes.
Happy Infrastructure as Code! 🚀