Advanced

Applying Terraform in Azure Environments

Terraform on Azure — provider auth, resource modeling, state, safe applies and drift diagnosis.

A comprehensive course on using Terraform in Azure environments — from operational foundations to production practices.


Table of Contents

  1. Terraform in the Azure Operational Context
  2. Provider Architecture and Authentication
  3. Resource Modeling and Azure Patterns
  4. Terraform State Management in Azure
  5. Planning and Applying Changes Safely
  6. Diagnosing Drift and Configuration Inconsistencies
  7. Production Best Practices
  8. Reference Diagrams
  9. 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:

PhaseDescriptionARM Interaction
terraform initDownloads the AzureRM provider, initializes the backendChecks connectivity to the backend storage
terraform planEvaluates config, builds the dependency graph, refreshes stateReads the current state of resources via ARM API
terraform applyExecutes the approved planSends CREATE/UPDATE/DELETE calls to ARM
terraform destroyDestroys all managed resourcesSends 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 login context.
  • 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:

MethodUsage contextConfiguration
Azure CLI (az login)Local developmentAutomatic 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 IdentityAzure-hosted runners, AKSuse_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 OperationMinimum role required
State read (plan)Reader
Create/modify resourcesContributor
Manage RBAC assignmentsUser Access Administrator
Full managementOwner
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:

ServiceDefault retention periodManual purge possible
Azure Key Vault90 daysYes (az keyvault purge)
Azure Storage (blobs)7–365 days (configurable)Yes
Azure SQL Database7 daysNo (automatic expiry)
Azure Cosmos DB30 daysYes

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:

PracticeDescription
Entra ID authenticationUse use_entra_id_auth = true instead of an access key
Blob versioningEnable to be able to roll back to a previous state
Soft delete (blobs)Enable to protect against accidental deletion
Restricted network accessLimit storage access to CI/CD runners and authorized developers
State file per environmentUse distinct keys (app1-dev.tfstate, app1-prod.tfstate)
RBAC on storageStorage 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:

SymbolMeaningRisk
+Create a new resourceLow
~In-place updateLow
-Destroy a resourceHigh
-/+Destroy then recreate (replacement)Very high
<=Read a data sourceNone

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:

ResourceImmutable properties
azurerm_virtual_networkname, location
azurerm_key_vaultname, location, tenant_id
azurerm_storage_accountname, location, account_kind
azurerm_linux_virtual_machinename, location, os_disk.storage_account_type (in some cases)
azurerm_subnetname, 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):

ScopeRequest limitPeriod
Subscription (reads)12,0005 minutes
Subscription (writes)1,2005 minutes
Resource group (reads)6,0005 minutes
Tenant (reads)12,0005 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 ResourceTerraform Resource TypeRequired Arguments
Resource Groupazurerm_resource_groupname, location
Virtual Networkazurerm_virtual_networkname, address_space, location, resource_group_name
Subnetazurerm_subnetname, resource_group_name, virtual_network_name, address_prefixes
Network Interfaceazurerm_network_interfacename, location, resource_group_name, ip_configuration
Linux VMazurerm_linux_virtual_machinename, resource_group_name, location, size, admin_username, network_interface_ids, os_disk, source_image_reference
Windows VMazurerm_windows_virtual_machineSame + admin_password
Storage Accountazurerm_storage_accountname, resource_group_name, location, account_tier, account_replication_type
Key Vaultazurerm_key_vaultname, location, resource_group_name, tenant_id, sku_name
App Service Planazurerm_service_planname, resource_group_name, location, os_type, sku_name
Web App (Linux)azurerm_linux_web_appname, resource_group_name, location, service_plan_id, site_config
SQL Serverazurerm_mssql_servername, resource_group_name, location, version, administrator_login, administrator_login_password
SQL Databaseazurerm_mssql_databasename, server_id
AKS Clusterazurerm_kubernetes_clustername, location, resource_group_name, dns_prefix, default_node_pool
Management Lockazurerm_management_lockname, scope, lock_level

Common Azure VM Sizes

SKUvCPURAMUse case
Standard_B1s11 GBDev/Test (bursting)
Standard_B2s24 GBDev/Test (bursting)
Standard_D2s_v328 GBGeneral purpose production
Standard_D4s_v3416 GBGeneral purpose production
Standard_E4s_v3432 GBMemory-optimized
Standard_F4s_v248 GBCompute-optimized
RegionTerraform NameAvailability
Canada East (Quebec)canadaeastFull
Canada Central (Toronto)canadacentralFull

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

Interested in this course?

Contact us to book it or get a custom training plan for your team.