A comprehensive course on using Terraform in Azure environments — from operational foundations to production practices.
Table of Contents
- Terraform in the Azure Operational Context
- Provider Architecture and Authentication
- Resource Modeling and Azure Patterns
- Terraform State Management in Azure
- Planning and Applying Changes Safely
- Diagnosing Drift and Configuration Inconsistencies
- Production Best Practices
- Reference Diagrams
- Azure Resource Reference Tables
1. Terraform in the Azure Operational Context
Mapping Terraform to the ARM Control Plane
When a Terraform deployment in Azure does not behave as expected, the problem is rarely isolated within the configuration itself. It is essential to understand how Terraform translates declarative intent into operations on the Azure Resource Manager (ARM) control plane.
Fundamental principle: Terraform operates as a declarative layer on top of ARM. Configuration is written in HashiCorp Configuration Language (HCL) and describes the desired final state of the infrastructure. Terraform evaluates this configuration and determines the required actions, then translates them via the AzureRM Provider into REST API calls to ARM.
flowchart LR
A[HCL Configuration] --> B[Terraform Core]
B --> C{Dependency Graph}
C --> D[AzureRM Provider]
D --> E[Azure Resource Manager REST API]
E --> F[(Azure Subscription)]
F --> G[Resource Groups]
G --> H[Azure Resources]
The Terraform workflow in Azure:
| Phase | Description | ARM Interaction |
|---|---|---|
terraform init | Downloads the AzureRM provider, initializes the backend | Checks connectivity to the backend storage |
terraform plan | Evaluates config, builds the dependency graph, refreshes state | Reads the current state of resources via ARM API |
terraform apply | Executes the approved plan | Sends CREATE/UPDATE/DELETE calls to ARM |
terraform destroy | Destroys all managed resources | Sends DELETE calls to ARM in reverse dependency graph order |
Key points:
- The same control plane is used by the Azure portal, Azure CLI, and ARM/Bicep templates.
- In local environments, authentication uses the
az logincontext. - In CI/CD (e.g., GitHub Actions), authentication relies on OpenID Connect (OIDC) with Microsoft Entra ID and a federated trust relationship.
- The identity used must have the required RBAC permissions on the target subscription, resource group, and backend storage account for remote state.
Defining Subscription and Tenant Boundaries
A correct configuration can still affect the wrong environment if the scope is not precisely defined.
Azure scope hierarchy:
graph TD
A[Tenant - Microsoft Entra ID] --> B[Root Management Group]
B --> C[Management Groups]
C --> D[Subscriptions]
D --> E[Resource Groups]
E --> F[Resources]
Key concepts:
- Tenant: Represents the identity boundary where the AzureRM provider authenticates via Microsoft Entra ID.
- Subscription: Primary boundary for isolating environments (billing, quotas, access control). Production and non-production workloads are commonly separated into distinct subscriptions.
- Resource Group: Resource container with a shared lifecycle.
- Management Groups: Allow organizing subscriptions and applying policies at scale.
Multi-subscription configuration in Terraform:
# Provider for the development subscription
provider "azurerm" {
alias = "dev"
subscription_id = var.dev_subscription_id
features {}
}
# Provider for the production subscription
provider "azurerm" {
alias = "prod"
subscription_id = var.prod_subscription_id
features {}
}
# Use a specific provider for a resource
resource "azurerm_resource_group" "prod_rg" {
provider = azurerm.prod
name = "rg-app1-prod"
location = "canadaeast"
}
2. Provider Architecture and Authentication
Configuring the AzureRM Provider
The AzureRM provider is the interface between Terraform and Azure Resource Manager. It translates HCL configuration into ARM API operations and manages authentication, subscription context, and Azure service behavior during provisioning.
Important notes on the features block:
- Required since AzureRM v4, even if empty.
- Allows configuration of service-level hooks (e.g., Key Vault soft delete, purge protection).
Minimal provider configuration:
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
required_version = ">= 1.5.0"
}
provider "azurerm" {
subscription_id = var.subscription_id
features {
key_vault {
purge_soft_delete_on_destroy = false
recover_soft_deleted_key_vaults = true
}
resource_group {
prevent_deletion_if_contains_resources = true
}
}
}
AzureRM provider authentication methods:
| Method | Usage context | Configuration |
|---|---|---|
Azure CLI (az login) | Local development | Automatic via CLI context |
| Service Principal (client secret) | CI/CD (legacy method) | client_id, client_secret, tenant_id |
| Service Principal (OIDC/Workload Identity) | Modern CI/CD (GitHub Actions, Azure Pipelines) | use_oidc = true + federated credentials |
| Managed Identity | Azure-hosted runners, AKS | use_msi = true |
Configuration for GitHub Actions with OIDC:
provider "azurerm" {
subscription_id = var.subscription_id
use_oidc = true
features {}
}
# .github/workflows/terraform.yml (excerpt)
- name: Azure Login via OIDC
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Terraform Apply
run: terraform apply -auto-approve
env:
ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ARM_USE_OIDC: "true"
Provider versioning and lock file:
The terraform.lock.hcl file tracks selected provider versions and ensures reproducibility:
# terraform.lock.hcl (auto-generated)
provider "registry.terraform.io/hashicorp/azurerm" {
version = "4.1.0"
constraints = "~> 4.0"
hashes = [
"h1:...",
]
}
Resolving Entra ID Authentication Failures
Critical distinction:
- Authentication error: The token could not be obtained (OIDC configuration issue, incorrect credentials).
- Authorization error (403 AuthorizationFailure): The token is valid but the identity does not have the necessary RBAC permissions.
Diagnostic workflow:
flowchart TD
A[terraform plan/apply fails] --> B{Failure phase?}
B -->|init| C[Backend connectivity or credentials issue]
B -->|plan| D{Error code?}
B -->|apply| D
D -->|401 Unauthorized| E[Authentication issue\nCheck OIDC federated credentials\nVerify client_id/tenant_id]
D -->|403 Forbidden| F[RBAC authorization issue\nCheck roles assigned\nto the identity on the target scope]
D -->|404 Not Found| G[Resource or scope not found\nCheck subscription_id\nCheck resource group name]
E --> H[Resolution]
F --> H
G --> H
Common RBAC roles required:
| Terraform Operation | Minimum role required |
|---|---|
| State read (plan) | Reader |
| Create/modify resources | Contributor |
| Manage RBAC assignments | User Access Administrator |
| Full management | Owner |
| Backend state access (Storage) | Storage Blob Data Contributor |
Assigning a role via Azure CLI:
# Assign the Contributor role to a Service Principal on a subscription
az role assignment create \
--assignee "<client-id>" \
--role "Contributor" \
--scope "/subscriptions/<subscription-id>"
# Assign the Storage Blob Data Contributor role for the backend
az role assignment create \
--assignee "<client-id>" \
--role "Storage Blob Data Contributor" \
--scope "/subscriptions/<subscription-id>/resourceGroups/<rg>/providers/Microsoft.Storage/storageAccounts/<storage-account>"
3. Resource Modeling and Azure Patterns
Managing the Resource Group Lifecycle
In Azure, everything is evaluated through the resource group, which becomes the unit where dependency resolution and deletion semantics converge.
Common pattern: Resource group managed by Terraform
# Resource group managed by Terraform
resource "azurerm_resource_group" "app" {
name = "rg-app1-dev"
location = "canadaeast"
tags = var.common_tags
}
# Virtual Network depending on the resource group
resource "azurerm_virtual_network" "main" {
name = "vnet-app1-dev"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.app.location
resource_group_name = azurerm_resource_group.app.name
tags = var.common_tags
}
# Subnet
resource "azurerm_subnet" "main" {
name = "snet-app1-dev"
resource_group_name = azurerm_resource_group.app.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.1.0/24"]
}
# Key Vault
resource "azurerm_key_vault" "main" {
name = "kv-app1-dev"
location = azurerm_resource_group.app.location
resource_group_name = azurerm_resource_group.app.name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
tags = var.common_tags
}
data "azurerm_client_config" "current" {}
Resulting dependency graph:
graph TD
RG[azurerm_resource_group.app] --> VNET[azurerm_virtual_network.main]
RG --> KV[azurerm_key_vault.main]
VNET --> SNET[azurerm_subnet.main]
Behavior during destruction: Terraform respects the reverse order of the dependency graph. If the resource group is destroyed, all its dependent resources are destroyed in the correct order.
lifecycle block to prevent accidental destruction:
resource "azurerm_resource_group" "app" {
name = "rg-app1-prod"
location = "canadaeast"
lifecycle {
prevent_destroy = true
}
}
Managing drift with ignore_changes:
resource "azurerm_virtual_machine" "main" {
# ...
lifecycle {
# Ignore tag changes managed by Azure Policy
ignore_changes = [tags]
}
}
Recovering from Soft-Delete Collisions
Some Azure services (Key Vault, Storage, Azure SQL) implement a soft delete model. When a resource is deleted, ARM does not immediately release the name — it retains a recoverable instance for a retention period.
Problem: Terraform attempts to recreate a resource whose name is still reserved at the ARM level → 409 Conflict error.
Diagnosis:
# List soft-deleted Key Vaults
az keyvault list-deleted --query "[].{Name:name, DeletionDate:properties.deletionDate, ScheduledPurgeDate:properties.scheduledPurgeDate}" -o table
# Permanently purge a soft-deleted Key Vault
az keyvault purge --name "kv-app1-dev" --location "canadaeast"
# Recover a soft-deleted Key Vault
az keyvault recover --name "kv-app1-dev"
Terraform configuration to handle soft-delete:
provider "azurerm" {
subscription_id = var.subscription_id
features {
key_vault {
# Automatically purge on destroy (use with caution in prod!)
purge_soft_delete_on_destroy = true
# Attempt to recover an existing soft-deleted vault
recover_soft_deleted_key_vaults = true
}
}
}
Azure services with soft-delete:
| Service | Default retention period | Manual purge possible |
|---|---|---|
| Azure Key Vault | 90 days | Yes (az keyvault purge) |
| Azure Storage (blobs) | 7–365 days (configurable) | Yes |
| Azure SQL Database | 7 days | No (automatic expiry) |
| Azure Cosmos DB | 30 days | Yes |
4. Terraform State Management in Azure
Securing Remote State in Blob Storage
The Terraform state file is the source of truth that maps the declared configuration to real Azure resources. In shared or CI/CD environments, remote state in Azure Blob Storage is indispensable.
Remote state architecture:
flowchart LR
subgraph "GitHub Actions / Pipeline"
TF[Terraform CLI]
end
subgraph "Azure Storage Account"
CONT[Container: tfstate]
BLOB[Blob: app1-dev.tfstate]
LOCK[Blob Lease = State Lock]
end
subgraph "Azure Subscription"
ARM[Azure Resource Manager]
RES[Managed resources]
end
TF -->|"terraform init\n(reads backend config)"| BLOB
TF -->|"terraform plan\n(reads state)"| BLOB
TF -->|"terraform apply\n(acquires lease = lock)"| LOCK
TF -->|"ARM API calls"| ARM
ARM --> RES
BLOB -.->|"State mapping"| RES
AzureRM backend configuration:
# backend.tf
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "stterraformstate001"
container_name = "tfstate"
key = "app1-dev.tfstate"
use_entra_id_auth = true # Authentication via Microsoft Entra ID
}
}
Creating the backend storage with Azure CLI:
#!/bin/bash
RESOURCE_GROUP="rg-terraform-state"
STORAGE_ACCOUNT="stterraformstate001"
CONTAINER="tfstate"
LOCATION="canadaeast"
# Create the resource group
az group create --name $RESOURCE_GROUP --location $LOCATION
# Create the storage account with enhanced security
az storage account create \
--name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--sku Standard_LRS \
--kind StorageV2 \
--min-tls-version TLS1_2 \
--allow-blob-public-access false \
--https-only true
# Enable blob versioning (state protection)
az storage account blob-service-properties update \
--account-name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--enable-versioning true
# Create the container
az storage container create \
--name $CONTAINER \
--account-name $STORAGE_ACCOUNT \
--auth-mode login
Remote state best practices:
| Practice | Description |
|---|---|
| Entra ID authentication | Use use_entra_id_auth = true instead of an access key |
| Blob versioning | Enable to be able to roll back to a previous state |
| Soft delete (blobs) | Enable to protect against accidental deletion |
| Restricted network access | Limit storage access to CI/CD runners and authorized developers |
| State file per environment | Use distinct keys (app1-dev.tfstate, app1-prod.tfstate) |
| RBAC on storage | Storage Blob Data Contributor for Terraform identities |
Breaking Stuck State Leases
Terraform protects the state file via a locking mechanism. In Azure, the AzureRM backend uses a blob lease — a server-side constraint on the state blob.
Blocking scenario: A pipeline terminates abruptly (CTRL+C, timeout, crash). The lease remains active. Any subsequent attempt fails with an error like:
Error: Error locking state: Error acquiring the state lock: storage: service returned error: StatusCode=409, ErrorCode=LeaseAlreadyPresent
Lock Info:
ID: <lock-uuid>
Path: app1-dev.tfstate
Operation: OperationTypeApply
...
Resolving the lock via Azure CLI:
# 1. Identify the locked blob
az storage blob show \
--account-name "stterraformstate001" \
--container-name "tfstate" \
--name "app1-dev.tfstate" \
--auth-mode login \
--query "{leaseStatus:properties.leaseStatus, leaseState:properties.leaseState}"
# 2. Break the lease manually
az storage blob lease break \
--account-name "stterraformstate001" \
--container-name "tfstate" \
--blob-name "app1-dev.tfstate" \
--auth-mode login
Alternative via Terraform (with the Lock ID):
terraform force-unlock <lock-id>
Warning: Only break a lock when you are certain no other Terraform process is running against this state.
5. Planning and Applying Changes Safely
Identifying Destructive Changes in the Plan
terraform plan exposes the exact behavior of each change before application. Understanding plan symbols is critical to avoiding production incidents.
Terraform plan symbols:
| Symbol | Meaning | Risk |
|---|---|---|
+ | Create a new resource | Low |
~ | In-place update | Low |
- | Destroy a resource | High |
-/+ | Destroy then recreate (replacement) | Very high |
<= | Read a data source | None |
Example plan with replacement:
# azurerm_virtual_network.main must be replaced
-/+ resource "azurerm_virtual_network" "main" {
~ name = "vnet-app1-dev" -> "vnet-app1-v2" # forces replacement
~ address_space = ["10.0.0.0/16"] -> ["10.1.0.0/16"] # forces replacement
location = "canadaeast"
...
}
Immutable properties (requiring replacement) in common Azure resources:
| Resource | Immutable properties |
|---|---|
azurerm_virtual_network | name, location |
azurerm_key_vault | name, location, tenant_id |
azurerm_storage_account | name, location, account_kind |
azurerm_linux_virtual_machine | name, location, os_disk.storage_account_type (in some cases) |
azurerm_subnet | name, virtual_network_name |
Using prevent_destroy for critical resources:
resource "azurerm_storage_account" "critical" {
name = "stapp1proddata"
resource_group_name = azurerm_resource_group.app.name
location = azurerm_resource_group.app.location
account_tier = "Standard"
account_replication_type = "GRS"
lifecycle {
prevent_destroy = true
}
}
Secure validation workflow:
flowchart TD
A[Modify the HCL configuration] --> B[terraform fmt]
B --> C[terraform validate]
C --> D[terraform plan -out=tfplan]
D --> E{Analyze the plan}
E -->|Destructive changes -/+| F[Mandatory review\nby the team]
E -->|Additive changes +| G[Standard approval]
F --> H{Approved?}
H -->|No| A
H -->|Yes| I[terraform apply tfplan]
G --> I
I --> J[Verify in the Azure portal]
Managing ARM API Throttling
Azure Resource Manager applies throttling limits to maintain platform stability. In large Terraform environments, the refresh phase alone can generate a high volume of ARM requests.
Throttling behavior:
- ARM returns HTTP 429 (Too Many Requests) when limits are reached.
- Terraform displays pauses between operations and retry attempts.
- The AzureRM provider has built-in retry logic with exponential backoff.
Provider parameters for managing throttling:
provider "azurerm" {
subscription_id = var.subscription_id
features {}
# Disable automatic provider registration to reduce ARM calls
# (use with caution)
skip_provider_registration = false
}
Strategies to reduce throttling:
# Use -refresh=false for scheduled applies (avoids the refresh)
# terraform apply -refresh=false tfplan
# Use -parallelism to limit concurrency
# terraform apply -parallelism=5 (default: 10)
# Split configurations into independent modules
# to reduce the number of resources per state file
Documented ARM limits (Azure subscription limits):
| Scope | Request limit | Period |
|---|---|---|
| Subscription (reads) | 12,000 | 5 minutes |
| Subscription (writes) | 1,200 | 5 minutes |
| Resource group (reads) | 6,000 | 5 minutes |
| Tenant (reads) | 12,000 | 5 minutes |
6. Diagnosing Drift and Configuration Inconsistencies
Detecting Changes Made Outside Terraform
Configuration drift refers to the situation where the infrastructure in Azure no longer matches what Terraform recorded in its state file. Terraform does not continuously monitor infrastructure — it evaluates differences only during a run.
Drift detection workflow:
sequenceDiagram
participant Dev as Developer
participant TF as Terraform
participant ARM as Azure ARM
participant State as State File (Blob)
Dev->>ARM: Manual change\n(Azure portal / CLI)
Note over ARM: Infrastructure modified\nwithout Terraform
Dev->>TF: terraform plan
TF->>State: Reads current state
TF->>ARM: Refresh: queries real resources
ARM->>TF: Returns current state (with drift)
TF->>TF: Compares config vs state vs reality
TF->>Dev: Displays differences (drift detected)
Example of detected drift:
# azurerm_linux_virtual_machine.main will be updated in-place
~ resource "azurerm_linux_virtual_machine" "main" {
id = "/subscriptions/.../virtualMachines/vm-app1-dev"
name = "vm-app1-dev"
...
os_disk {
~ storage_account_type = "Premium_LRS" -> "Standard_LRS" # drift detected
}
}
Useful commands for managing drift:
# Refresh only the state (without planning changes)
terraform refresh
# Plan that explicitly shows drift
terraform plan -refresh=true
# Display the current state of a specific resource
terraform state show azurerm_linux_virtual_machine.main
Performing a Declarative Resource Import
Resources created manually in Azure (portal, scripts) can be placed under Terraform control via import.
Prerequisite: Obtain the Azure Resource ID of the resource.
# Find the ID of a storage account
az storage account show \
--name "stapp1devdata" \
--resource-group "rg-app1-dev" \
--query id -o tsv
# Output: /subscriptions/<sub-id>/resourceGroups/rg-app1-dev/providers/Microsoft.Storage/storageAccounts/stapp1devdata
Method 1: Declarative import block (Terraform >= 1.5):
# import.tf - Import declaration in the configuration
import {
to = azurerm_storage_account.imported
id = "/subscriptions/<sub-id>/resourceGroups/rg-app1-dev/providers/Microsoft.Storage/storageAccounts/stapp1devdata"
}
# Optional with Terraform >= 1.5: auto-generate config
# terraform plan -generate-config-out=generated.tf
Method 2: Classic CLI import:
# Import the resource into the state
terraform import azurerm_storage_account.imported \
"/subscriptions/<sub-id>/resourceGroups/rg-app1-dev/providers/Microsoft.Storage/storageAccounts/stapp1devdata"
Corresponding HCL configuration:
resource "azurerm_storage_account" "imported" {
name = "stapp1devdata"
resource_group_name = "rg-app1-dev"
location = "canadaeast"
account_tier = "Standard"
account_replication_type = "LRS"
tags = var.common_tags
}
Complete import workflow:
flowchart TD
A[Existing resource in Azure\nnot managed by Terraform] --> B[Identify the Azure Resource ID]
B --> C[Write the HCL resource block]
C --> D{Import method}
D -->|Terraform >= 1.5| E[Add import block\n+ terraform plan -generate-config-out]
D -->|Classic CLI| F[terraform import resource.name /azure/resource/id]
E --> G[terraform plan\nVerify = no changes]
F --> G
G -->|Changes detected| H[Adjust the HCL config]
H --> G
G -->|No changes| I[Resource under Terraform control]
State management commands:
# List all resources in the state
terraform state list
# Remove a resource from the state (without destroying it in Azure)
terraform state rm azurerm_storage_account.old_resource
# Move a resource in the state (rename)
terraform state mv azurerm_storage_account.old azurerm_storage_account.new
7. Production Best Practices
Guardrails: Locks and Tags
In shared Azure environments, two protection mechanisms are essential for production promotion.
1. Azure Management Locks
Resource locks prevent accidental deletion at the ARM control plane level.
# CanNotDelete lock on the resource group
resource "azurerm_management_lock" "rg_lock" {
name = "lock-rg-app1-prod"
scope = azurerm_resource_group.app.id
lock_level = "CanNotDelete"
notes = "Protects the production resource group from accidental deletion"
}
# Available lock types
# "CanNotDelete" : read and modify allowed, deletion blocked
# "ReadOnly" : read-only (modifications also blocked)
2. Consistent tagging with locals
# variables.tf
variable "environment" {
description = "Deployment environment"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "project" {
description = "Project name"
type = string
}
# locals.tf - Shared tags defined once
locals {
common_tags = {
Environment = var.environment
Project = var.project
ManagedBy = "Terraform"
CreatedDate = formatdate("YYYY-MM-DD", timestamp())
Owner = var.owner_email
}
}
# Usage in resources
resource "azurerm_resource_group" "app" {
name = "rg-${var.project}-${var.environment}"
location = var.location
tags = local.common_tags
}
resource "azurerm_virtual_network" "main" {
name = "vnet-${var.project}-${var.environment}"
address_space = [var.vnet_address_space]
location = azurerm_resource_group.app.location
resource_group_name = azurerm_resource_group.app.name
tags = local.common_tags
}
Complete production module structure:
infrastructure/
├── main.tf # Main resources
├── variables.tf # Variable declarations
├── outputs.tf # Exported outputs
├── locals.tf # Locals (tags, computed names)
├── providers.tf # Provider configuration
├── backend.tf # Remote state configuration
├── terraform.tfvars # Values (not committed in prod)
└── modules/
├── networking/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── compute/
├── main.tf
├── variables.tf
└── outputs.tf
Pre-flight Check for Staging to Production Promotion
Before promoting a configuration from staging to production, multi-layer validation is required.
Layer 1: Terraform scope — target subscription verification
# Explicit subscription check in production
provider "azurerm" {
subscription_id = var.subscription_id # Always explicit, never implicit
features {}
}
# Variable with validation
variable "subscription_id" {
description = "ID of the target Azure subscription"
type = string
validation {
condition = can(regex("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", var.subscription_id))
error_message = "The subscription_id must be a valid UUID."
}
}
Layer 2: Azure authentication context
# Verify the active identity before any operation
az account show --query "{subscriptionId:id, subscriptionName:name, tenantId:tenantId}" -o table
# Verify RBAC permissions of the current identity
az role assignment list --assignee $(az ad signed-in-user show --query id -o tsv) --all --query "[].{Role:roleDefinitionName, Scope:scope}" -o table
Layer 3: Lifecycle guardrails in production
# terraform.tfvars for production
environment = "prod"
subscription_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# With prevent_destroy enabled for critical resources
# and management_lock on sensitive resource groups
Complete pre-flight checklist:
flowchart TD
A[Start Production Pre-flight Check] --> B[1. Verify subscription_id in the config]
B --> C[2. Verify current identity\naz account show]
C --> D[3. Verify RBAC assignments]
D --> E[4. Verify prevent_destroy is enabled\nfor critical resources]
E --> F[5. Verify management_locks\nare in place]
F --> G[6. terraform plan on the prod environment]
G --> H{Analyze the plan}
H -->|Destructive changes -/+ detected| I[Mandatory escalation and review]
H -->|Additive or minor update changes| J[Senior team approval]
I --> K{Approved?}
J --> K
K -->|Yes| L[terraform apply -auto-approve=false]
K -->|No| M[Return to staging]
L --> N[Post-deployment verification\nAzure Portal + Monitoring]
Complete CI/CD pipeline for staging → production:
# .github/workflows/terraform-prod.yml
name: Terraform Production Deploy
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
plan:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Azure Login (OIDC)
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "~> 1.9"
- name: Terraform Init
run: terraform init
env:
ARM_USE_OIDC: "true"
ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Terraform Plan
run: terraform plan -out=tfplan -var="subscription_id=${{ secrets.AZURE_SUBSCRIPTION_ID }}"
env:
ARM_USE_OIDC: "true"
- name: Upload Plan Artifact
uses: actions/upload-artifact@v4
with:
name: tfplan
path: tfplan
apply:
needs: plan
runs-on: ubuntu-latest
environment: production # Requires manual approval in GitHub
steps:
- name: Download Plan
uses: actions/download-artifact@v4
with:
name: tfplan
- name: Terraform Apply
run: terraform apply tfplan
env:
ARM_USE_OIDC: "true"
8. Reference Diagrams
Complete Terraform Workflow
stateDiagram-v2
[*] --> Init : terraform init
Init --> Validate : terraform validate
Validate --> Plan : terraform plan
Plan --> Review : Human review of the plan
Review --> Apply : terraform apply (approved)
Review --> Plan : Changes required
Apply --> Deployed : Success
Apply --> Plan : Failure / Corrections
Deployed --> Plan : Configuration changes
Deployed --> Destroy : terraform destroy
Destroy --> [*]
Typical Azure Architecture Managed by Terraform
graph TB
subgraph "Azure Subscription - Production"
subgraph "rg-app1-prod"
VNET[Virtual Network\nvnet-app1-prod\n10.0.0.0/16]
SNET1[Subnet: snet-web\n10.0.1.0/24]
SNET2[Subnet: snet-app\n10.0.2.0/24]
SNET3[Subnet: snet-data\n10.0.3.0/24]
VM[Linux VM\nvm-app1-prod]
NIC[Network Interface]
KV[Key Vault\nkv-app1-prod]
ST[Storage Account\nstapp1proddata]
LOCK[Management Lock\nCanNotDelete]
end
subgraph "rg-terraform-state"
STSTATE[Storage Account\nRemote State]
CONT[Container: tfstate]
end
end
VNET --> SNET1
VNET --> SNET2
VNET --> SNET3
SNET2 --> NIC
NIC --> VM
VM --> KV
VM --> ST
LOCK -.-> VNET
CONT --> STSTATE
State Management Flow
sequenceDiagram
participant CI as CI/CD Pipeline
participant TF as Terraform
participant BLOB as Azure Blob Storage\n(Remote State)
participant ARM as Azure Resource Manager
CI->>TF: terraform init
TF->>BLOB: Initializes backend connection
BLOB-->>TF: Backend ready confirmation
CI->>TF: terraform plan
TF->>BLOB: Reads current state
TF->>ARM: Refresh existing resources
ARM-->>TF: Real resource state
TF-->>CI: Displays the plan (diff)
CI->>TF: terraform apply
TF->>BLOB: Acquires blob lease (LOCK)
TF->>ARM: Creates/Modifies/Deletes resources
ARM-->>TF: Operation confirmations
TF->>BLOB: Updates the state file
TF->>BLOB: Releases the lease (UNLOCK)
TF-->>CI: Apply completed successfully
9. Azure Resource Reference Tables
Common Azure Resources with Their Terraform Provider
| Azure Resource | Terraform Resource Type | Required Arguments |
|---|---|---|
| Resource Group | azurerm_resource_group | name, location |
| Virtual Network | azurerm_virtual_network | name, address_space, location, resource_group_name |
| Subnet | azurerm_subnet | name, resource_group_name, virtual_network_name, address_prefixes |
| Network Interface | azurerm_network_interface | name, location, resource_group_name, ip_configuration |
| Linux VM | azurerm_linux_virtual_machine | name, resource_group_name, location, size, admin_username, network_interface_ids, os_disk, source_image_reference |
| Windows VM | azurerm_windows_virtual_machine | Same + admin_password |
| Storage Account | azurerm_storage_account | name, resource_group_name, location, account_tier, account_replication_type |
| Key Vault | azurerm_key_vault | name, location, resource_group_name, tenant_id, sku_name |
| App Service Plan | azurerm_service_plan | name, resource_group_name, location, os_type, sku_name |
| Web App (Linux) | azurerm_linux_web_app | name, resource_group_name, location, service_plan_id, site_config |
| SQL Server | azurerm_mssql_server | name, resource_group_name, location, version, administrator_login, administrator_login_password |
| SQL Database | azurerm_mssql_database | name, server_id |
| AKS Cluster | azurerm_kubernetes_cluster | name, location, resource_group_name, dns_prefix, default_node_pool |
| Management Lock | azurerm_management_lock | name, scope, lock_level |
Common Azure VM Sizes
| SKU | vCPU | RAM | Use case |
|---|---|---|---|
Standard_B1s | 1 | 1 GB | Dev/Test (bursting) |
Standard_B2s | 2 | 4 GB | Dev/Test (bursting) |
Standard_D2s_v3 | 2 | 8 GB | General purpose production |
Standard_D4s_v3 | 4 | 16 GB | General purpose production |
Standard_E4s_v3 | 4 | 32 GB | Memory-optimized |
Standard_F4s_v2 | 4 | 8 GB | Compute-optimized |
Recommended Azure Regions for Canada
| Region | Terraform Name | Availability |
|---|---|---|
| Canada East (Quebec) | canadaeast | Full |
| Canada Central (Toronto) | canadacentral | Full |
Quick-Reference Terraform Commands
# Initialization and validation
terraform init # Initialize the workspace
terraform init -upgrade # Update providers
terraform validate # Validate HCL syntax
terraform fmt -recursive # Format all HCL files
# Plan and apply
terraform plan # Display planned changes
terraform plan -out=tfplan # Save the plan to a file
terraform plan -var-file=prod.tfvars # Use a variables file
terraform apply # Apply changes (with confirmation)
terraform apply tfplan # Apply a saved plan
terraform apply -auto-approve # Apply without confirmation (CI/CD only)
terraform apply -parallelism=5 # Limit concurrency (anti-throttling)
# State management
terraform state list # List all resources in the state
terraform state show <resource> # Display the state of a resource
terraform state rm <resource> # Remove from state (without destroying)
terraform state mv <old> <new> # Rename in the state
terraform refresh # Sync state with Azure
terraform import <resource> <azure-id> # Import an existing resource
# Destruction
terraform destroy # Destroy all infrastructure
terraform destroy -target=<resource> # Destroy a specific resource
Search Terms
applying · terraform · azure · environments · infrastructure · ci/cd · devops · resource · state · changes · provider · architecture · arm · authentication · management · managing · production · reference