Advanced

Implementing Terraform on Microsoft Azure

The AzureRM provider, multiple providers, remote state on Azure, Azure DevOps and data sources.

Prerequisites: Intermediate knowledge of Terraform and Microsoft Azure (IaaS: Storage, Networking, Compute, Azure AD)
Exercise files: GitHub — ned1313/Implementing-Terraform-on-Microsoft-Azure


Table of Contents

  1. Course Overview
  2. Module 2 — Using the AzureRM Provider
  3. Module 3 — Creating Multiple Providers
  4. Module 4 — Using Azure for Remote State
  5. Module 5 — Using Azure DevOps
  6. Module 6 — Using Data Sources and ARM Templates
  7. Architecture Diagrams
  8. Reference Tables
  9. HCL Code Reference Snippets
  10. Summary and Next Steps

1. Course Overview

This course covers the use of HashiCorp Terraform to automate infrastructure deployment on Microsoft Azure. The main topics covered are:

TopicDescription
Azure ProvidersConfiguration, authentication and usage of the AzureRM and Azure AD providers
Multiple ProvidersUsing multiple instances in the same configuration
Remote StateStoring state in Azure Blob Storage
Azure DevOpsAutomating deployments with CI/CD pipelines
Data Sources & ARM TemplatesConsuming external data and integrating ARM templates

2. Module 2 — Using the AzureRM Provider

Terraform vs ARM Templates

The module begins with a comparison between ARM Templates and Terraform to ease the transition for Azure administrators:

AspectARM TemplatesTerraform (HCL)
LanguageJSONHashiCorp Configuration Language (HCL)
CommentsNot natively supportedSupported (#, //, /* */)
ReadabilityVerbose, difficult to readMore human-readable
Parametersparameters (values submitted to the template)variables
Internal variablesvariables (values defined within the template)locals (locals {} block)
Resourcesresourcesresources
FunctionsJSON functionsBuilt-in HCL functions
External referencesNested templatesTerraform modules

Common point of confusion: What ARM calls parameters = variables in Terraform. What ARM calls variables = locals in Terraform.


Globomantics Scenario

Throughout the course, the Globomantics company scenario is used:

  • Profile: Logistics company for restaurants, migrating to Azure
  • Project team:
    • Chris Jones — Cloud Architect (infrastructure deployment)
    • Danny Brown — Security Administrator (separate security subscription)
    • Hector Sanchez — Software Engineer (application deployment)

Azure Providers

There are three Azure providers in Terraform:

ProviderHCL NameUsage
AzureRMazurermAzure Public Cloud, Azure Gov, Sovereign Clouds — uses the ARM API
Azure StackazurestackOn-premises extension of Azure (same ARM API, different versions)
Azure Active DirectoryazureadAzure AD (AAD) management — dedicated provider since AAD grew in complexity

Note: The legacy ASM (Azure Service Manager) provider is deprecated.


AzureRM Provider Configuration

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}

provider "azurerm" {
  features {}

  # Optional — target subscription identifier
  subscription_id = var.subscription_id

  # Authentication via Service Principal
  client_id       = var.client_id
  client_secret   = var.client_secret
  tenant_id       = var.tenant_id

  # Alias for multiple instances
  # alias = "security"
}

Components of a Terraform provider:

  • Version: Specify a version constraint to avoid breaking changes
  • Alias: Allows multiple instances of the same provider
  • Data Sources: Information extracted from the target environment (e.g. list of Marketplace images)
  • Resources: Resources that can be created in the target environment

Deploying a Virtual Network

First deployment for Globomantics — creating a VNet with two subnets (web and database) via Azure Cloud Shell:

# variables.tf
variable "resource_group_name" {
  type = string
  # No default — required at runtime
}

variable "location" {
  type    = string
  default = "eastus"
}

variable "vnet_cidr_range" {
  type    = string
  default = "10.0.0.0/16"
}

variable "subnet_prefixes" {
  type    = list(string)
  default = ["10.0.0.0/24", "10.0.1.0/24"]
}

variable "subnet_names" {
  type    = list(string)
  default = ["web", "database"]
}
# main.tf — Using the Azure/vnet/azurerm module
provider "azurerm" {
  features {}
  # Authentication via Azure CLI (Cloud Shell)
}

module "vnet" {
  source  = "Azure/vnet/azurerm"
  version = "~> 2.0"

  resource_group_name = var.resource_group_name
  vnet_location       = var.location
  address_space       = [var.vnet_cidr_range]
  subnet_prefixes     = var.subnet_prefixes
  subnet_names        = var.subnet_names

  tags = {
    environment = "development"
    project     = "globomantics"
  }
}

resource "azurerm_resource_group" "main" {
  name     = var.resource_group_name
  location = var.location
}

Advantages of Azure Cloud Shell:

  • Terraform pre-installed
  • Azure CLI pre-installed
  • Automatic authentication via Azure AD credentials used for the portal

Authentication

Terraform supports several authentication methods for the AzureRM provider:

MethodDescriptionRecommended Usage
Azure CLIaz login — uses CLI tokensLocal development, Cloud Shell
Service Principal + Client Secretclient_id + client_secret + tenant_idCI/CD pipelines
Service Principal + Client CertificateCertificate instead of secretSecure environments
Managed Service Identity (MSI)Identity assigned to an Azure VMAzure VMs (no credentials)
# Via environment variables (recommended for CI/CD)
# export ARM_CLIENT_ID="..."
# export ARM_CLIENT_SECRET="..."
# export ARM_SUBSCRIPTION_ID="..."
# export ARM_TENANT_ID="..."
# export ARM_USE_MSI=true  # for MSI

3. Module 3 — Creating Multiple Providers

Multiple instances of the same provider

There are two main reasons to use multiple instances of the AzureRM provider:

  1. Multi-subscription: Each provider instance is specific to a subscription
  2. Multi-authentication: Different authentication sources for different resources
# Default provider (dev subscription)
provider "azurerm" {
  features {}
  # All resources without an explicit "provider" use this instance
}

# Provider for the security subscription
provider "azurerm" {
  alias           = "security"
  features        {}
  subscription_id = var.sec_sub_id
  client_id       = var.sec_client_id
  client_secret   = var.client_secret

  # Avoids errors if the SP has limited permissions
  skip_provider_registration    = true
  skip_credentials_validation   = true
}

# Provider for peering
provider "azurerm" {
  alias           = "peering"
  features        {}
  subscription_id = data.azurerm_subscription.current.id
  client_id       = var.sec_client_id
  client_secret   = var.client_secret
}

Referencing a specific provider in a resource:

resource "azurerm_virtual_network" "security" {
  provider = azurerm.security  # References the alias

  name                = "security-vnet"
  resource_group_name = azurerm_resource_group.security.name
  location            = var.location
  address_space       = ["10.1.0.0/16"]
}

Referencing a provider in a module:

module "security_vnet" {
  source = "Azure/vnet/azurerm"

  providers = {
    azurerm = azurerm.security  # Provider alias mapping
  }

  resource_group_name = azurerm_resource_group.security.name
  vnet_location       = var.location
  address_space       = ["10.1.0.0/16"]
  subnet_prefixes     = ["10.1.0.0/24", "10.1.1.0/24"]
  subnet_names        = ["siem", "inspect"]
}

Azure AD Provider

provider "azuread" {
  # Pre-1.0 version at time of course
  # version = "~> 0.6"

  tenant_id = var.tenant_id

  # Uses the same ARM_* environment variables as AzureRM
  # ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID, etc.
}

# Create a Service Principal in Azure AD
resource "azuread_application" "sp_app" {
  display_name = "globomantics-security-sp"
}

resource "azuread_service_principal" "sp" {
  application_id = azuread_application.sp_app.application_id
}

resource "azuread_service_principal_password" "sp_password" {
  service_principal_id = azuread_service_principal.sp.id
  value                = var.sp_password
  end_date             = "2025-01-01T00:00:00Z"
}

Multi-subscription VNet Peering

The Globomantics scenario requires peering between the dev subscription (Chris Jones) and the security subscription (Danny Brown):

# vnet-peering.tf

# Data source to get the current subscription
data "azurerm_subscription" "current" {}

# Retrieve the existing security VNet
data "azurerm_virtual_network" "security" {
  provider            = azurerm.security
  name                = var.sec_vnet_name
  resource_group_name = var.sec_resource_group
}

# Retrieve the existing dev VNet
data "azurerm_virtual_network" "main" {
  name                = "main-vnet"
  resource_group_name = var.resource_group_name
}

# Peering from dev -> security side
resource "azurerm_virtual_network_peering" "dev_to_security" {
  name                      = "dev-to-security"
  resource_group_name       = var.resource_group_name
  virtual_network_name      = data.azurerm_virtual_network.main.name
  remote_virtual_network_id = data.azurerm_virtual_network.security.id
  allow_forwarded_traffic   = true
}

# Peering from security -> dev side (SP with limited permissions)
resource "azurerm_virtual_network_peering" "security_to_dev" {
  provider = azurerm.peering

  name                      = "security-to-dev"
  resource_group_name       = var.sec_resource_group
  virtual_network_name      = var.sec_vnet_name
  remote_virtual_network_id = data.azurerm_virtual_network.main.id
  allow_forwarded_traffic   = true
}

TF_VAR_ environment variables to pass values:*

export TF_VAR_sec_sub_id="<security-subscription-id>"
export TF_VAR_sec_client_id="<service-principal-client-id>"
export TF_VAR_client_secret="<service-principal-secret>"
export TF_VAR_sec_vnet_name="security-vnet"
export TF_VAR_sec_resource_group="security-rg"

4. Module 4 — Using Azure for Remote State

Remote State — core concepts

Two main reasons to use Remote State:

  1. Securing the state file — Avoid loss on a failing local disk
  2. Team collaboration — Sharing a single configuration and state file

Advanced features (depending on the backend):

FeatureDescription
LockingState locking during plan, apply, destroy — prevents concurrent modifications
WorkspacesMultiple environments (dev, UAT, prod) with the same code, separate state files
VersioningVersion history of the state file

Azure Blob Storage as backend

Azure Blob Storage supports:

  • Locking (via an additional lock file)
  • Workspaces (key named <base-key>.<workspace-name>)
  • Multiple authentication methods
# Deploy the Storage Account for remote state
resource "random_integer" "storage_suffix" {
  min = 10000
  max = 99999
}

resource "azurerm_resource_group" "state" {
  name     = var.resource_group_name
  location = var.location
}

resource "azurerm_storage_account" "state" {
  name                     = "${lower(var.naming_prefix)}${random_integer.storage_suffix.result}"
  resource_group_name      = azurerm_resource_group.state.name
  location                 = azurerm_resource_group.state.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_storage_container" "state" {
  name                  = "terraform-state"
  storage_account_name  = azurerm_storage_account.state.name
  container_access_type = "private"
}

# Generate a SAS Token for limited access
data "azurerm_storage_account_sas" "state" {
  connection_string = azurerm_storage_account.state.primary_connection_string
  https_only        = true

  resource_types {
    service   = true
    container = true
    object    = true
  }

  services {
    blob  = true
    queue = false
    table = false
    file  = false
  }

  start  = timestamp()
  expiry = timeadd(timestamp(), "8760h")  # 1 year

  permissions {
    read    = true
    write   = true
    delete  = false
    list    = true
    add     = true
    create  = true
    update  = false
    process = false
    tag     = false
    filter  = false
  }
}

Authentication methods for the AzureRM backend:

MethodHCL ArgumentRecommendation
Managed Service Identityuse_msi = trueAzure VMs with assigned MSI
SAS Tokensas_token = "..."Limited access with expiration ✅ Recommended
Storage Access Keyaccess_key = "..."Full access — not recommended
Service Principalclient_id, client_secretCI/CD pipelines

Migrating to Remote State

The backend configuration uses a terraform {} block — it does not accept variables (evaluated before variable parsing):

# backend.tf
terraform {
  backend "azurerm" {
    # Values can be provided via --backend-config instead of being hard-coded
    # storage_account_name = "itma12345"
    # container_name       = "terraform-state"
    # key                  = "networking/terraform.tfstate"
    # sas_token            = "..."
  }
}

backend-config.txt file (passed via --backend-config):

storage_account_name = "itma12345"
container_name       = "terraform-state"
key                  = "networking/terraform.tfstate"
sas_token            = "?sv=2019-10-10&ss=b&srt=sco&sp=rwlac..."

Migration process:

# 1. Add/modify the backend configuration in backend.tf
# 2. Run terraform init to reconfigure the backend
terraform init --backend-config=backend-config.txt

# Terraform will ask:
# "Do you want to copy existing state to the new backend? (yes/no)"
# Answer "yes" to migrate local state to Azure Blob Storage

5. Module 5 — Using Azure DevOps

Infrastructure as Code Fundamentals

The core IaC principles:

PrincipleDescription
Software-definedInfrastructure is defined by code (Terraform, ARM, Ansible…)
ReusableReusable modules and templates across different contexts
RepeatableSame configuration deployable identically across multiple environments
Source ControlInfrastructure code versioned in Git
AutomatedAutomated deployments via CI/CD pipelines

Source Control Management

Common SCM formats:

  • Git — the most popular
  • Team Foundation Version Control (TFVC) — formerly SourceSafe
  • Subversion (SVN) — common in open source shops

SCM Platforms:

PlatformTypeNotes
GitHubCloud-hostedFree private repositories
Azure DevOps ReposCloud-hostedIntegrated into the Azure ecosystem
GitLabCloud-hosted / Self-hostedIntegrated CI/CD
BitbucketCloud-hostedIntegrated with Atlassian (Jira)
Self-hosted GitSelf-managedFull control

Azure DevOps Repos and Pipelines

Azure DevOps structure:

Azure DevOps Organization
└── Project (e.g. globomantics-testing)
    ├── Boards     — Work tracking (Kanban, Scrum)
    ├── Repos      — Git/TFVC Repositories
    ├── Pipelines  — CI/CD Build & Release
    ├── Test Plans — QA management
    └── Artifacts  — Private dependency feeds

Azure DevOps pipeline types:

TypeUsage
Build PipelineCompilation, tests, artifact creation
Release PipelineDeploying artifacts to environments

For Terraform IaC, a Release Pipeline is sufficient (no compilation needed).

Terraform tasks in a Release Pipeline:

Agent Job (Ubuntu 16.04)
├── 1. Download Artifacts    — Retrieve source code from Repos
├── 2. Install Terraform     — Install Terraform CLI on the agent
├── 3. terraform init        — Initialize backend and providers
├── 4. Bash Script           — Select workspace (terraform workspace select)
├── 5. terraform plan        — Plan changes
└── 6. terraform apply       — Apply changes

Terraform Workspaces

Workspaces allow having multiple state files for the same configuration (one per environment):

# Reference the current workspace in the config
locals {
  full_rg_name = "${terraform.workspace}-${var.resource_group_name}"
  # Example: "development-vnet" or "uat-vnet" or "production-vnet"
}

# CIDR map by workspace
variable "vnet_cidr_range" {
  type = map(string)
  default = {
    development = "10.0.0.0/16"
    uat         = "10.2.0.0/16"
    production  = "10.4.0.0/16"
  }
}

# Use the current workspace value
resource "azurerm_virtual_network" "main" {
  name                = "${terraform.workspace}-vnet"
  resource_group_name = azurerm_resource_group.main.name
  location            = var.location
  address_space       = [lookup(var.vnet_cidr_range, terraform.workspace, "10.0.0.0/16")]
}

Workspace commands:

terraform workspace list              # List workspaces
terraform workspace new development   # Create dev workspace
terraform workspace select uat        # Switch to UAT
terraform workspace show              # Show active workspace

State storage in Azure Blob Storage with workspaces:

terraform-state container
├── networking/terraform.tfstate              # workspace: default
├── networking/terraform.tfstate.development  # workspace: development
├── networking/terraform.tfstate.uat          # workspace: uat
└── networking/terraform.tfstate.production   # workspace: production

Multi-environment CD Pipeline

Configuring a three-stage release pipeline in Azure DevOps:

Artifacts (Azure Repos)
    │
    ▼ Continuous Deployment Trigger
┌─────────────────┐
│  Development    │ ← After release (automatic)
│  Stage          │
└────────┬────────┘
         │ If successful
         ▼
┌─────────────────┐
│  UAT            │ ← After Development stage (automatic)
│  Stage          │
└────────┬────────┘
         │ If successful
         ▼
┌─────────────────┐
│  Production     │ ← Pre-deployment approval (manual — Chris Jones)
│  Stage          │
└─────────────────┘

Storing secrets in Azure DevOps:

  • Sensitive values (SAS tokens, client secrets) stored in Pipeline Variables (encrypted)
  • Non-sensitive values in the terraform.tfvars file in source control

6. Module 6 — Using Data Sources and ARM Templates

Layering Configurations

Layering involves decomposing infrastructure into atomic and independent configurations, linked via data sources:

Configuration 1: Networking
├── Virtual Network
├── Subnets
└── VNet Peering
        ↓ (remote state data source)
Configuration 2: Application (App Tier)
├── Internal Load Balancer
├── VM 1
└── VM 2
        ↓ (remote state data source)
Configuration 3: Web Frontend
├── App Service Plan
├── App Service
└── Subnet Delegation

Principle: Atomic and loosely coupled configurations rather than a fragile monolithic configuration.


Remote State as Data Source

# Consume the remote state from the networking configuration
data "terraform_remote_state" "networking" {
  backend = "azurerm"

  config = {
    storage_account_name = var.network_state["sa"]
    container_name       = var.network_state["cn"]
    key                  = var.network_state["key"]
    sas_token            = var.network_state["sts"]
  }
}

# Use outputs from the networking configuration
resource "azurerm_network_interface" "vm" {
  count               = var.vm_count
  name                = "${local.prefix}-nic-${count.index}"
  resource_group_name = azurerm_resource_group.app.name
  location            = var.location

  ip_configuration {
    name                          = "ipconfig"
    subnet_id                     = data.terraform_remote_state.networking.outputs.subnet_ids["app"]
    private_ip_address_allocation = "Dynamic"
  }
}

# Variables to access the networking state
variable "network_state" {
  type        = map(string)
  description = "Keys: sa (storage account), cn (container name), key (state key), sts (SAS token)"
}

Outputs to define in the networking configuration:

# outputs.tf — networking configuration
output "vnet_id" {
  value = module.vnet.vnet_id
}

output "subnet_ids" {
  value = module.vnet.vnet_subnets_name_id
}

output "vnet_address_space" {
  value = module.vnet.vnet_address_space
}

ARM Templates in Terraform

Use cases for ARM Templates in Terraform:

  1. Existing templates not yet refactored
  2. Azure resources not yet supported by the AzureRM provider
# Deploy an ARM Template via Terraform
resource "azurerm_resource_group_template_deployment" "app_service" {
  name                = "app-service-deployment"
  resource_group_name = azurerm_resource_group.app.name
  deployment_mode     = "Incremental"

  # ARM Template loaded from a file
  template_content = file("${path.module}/azuredeploy.json")

  # Parameters passed to the template (JSON format)
  parameters_content = jsonencode({
    webAppName = {
      value = "${local.prefix}-webapp"
    }
    sku = {
      value = "S1"
    }
    location = {
      value = var.location
    }
    vnetName = {
      value = data.azurerm_virtual_network.main.name
    }
    subnetId = {
      value = azurerm_subnet.appservice.id
    }
  })
}

Subnet Delegation for App Service:

resource "azurerm_subnet" "appservice" {
  name                 = "appservice"
  resource_group_name  = var.resource_group_name
  virtual_network_name = module.vnet.vnet_name

  # Dynamically calculate the next available range
  address_prefixes = [
    cidrsubnet(
      module.vnet.vnet_address_space[0],
      8,
      length(module.vnet.vnet_subnets_name_id)
    )
  ]

  delegation {
    name = "appservice-delegation"

    service_delegation {
      name    = "Microsoft.Web/serverFarms"
      actions = ["Microsoft.Network/virtualNetworks/subnets/action"]
    }
  }
}

7. Architecture Diagrams

Terraform Workflow

flowchart TD
    A([HCL code written]) --> B[terraform init]
    B --> C{Backend configured?}
    C -- No --> D[Local state]
    C -- Yes --> E[Azure Blob Storage]
    D --> F[terraform plan]
    E --> F
    F --> G[Plan displayed\nto the user]
    G --> H{Approval}
    H -- No --> A
    H -- Yes --> I[terraform apply]
    I --> J[Azure Resource Manager API]
    J --> K[(State file updated)]
    K --> L{Error?}
    L -- Yes --> M[Partial rollback\nInconsistent state]
    L -- No --> N([Infrastructure deployed ✅])
    
    O[terraform destroy] --> J

    style A fill:#7B68EE,color:#fff
    style N fill:#2E8B57,color:#fff
    style M fill:#DC143C,color:#fff
    style E fill:#0078D4,color:#fff

Azure Architecture — VNet, Subnets, NSG, VMs, App Services

flowchart TB
    subgraph SUB_DEV["Subscription: Development"]
        subgraph RG_VNET["Resource Group: development-vnet"]
            VNET["Virtual Network\n10.0.0.0/16"]
            subgraph SUBNET_WEB["Subnet: web\n10.0.0.0/24"]
                NSG_WEB["NSG: web-nsg"]
            end
            subgraph SUBNET_DB["Subnet: database\n10.0.1.0/24"]
                NSG_DB["NSG: db-nsg"]
            end
            subgraph SUBNET_APP["Subnet: app\n10.0.2.0/24"]
                LB["Internal Load Balancer"]
                VM1["VM: app-vm-0"]
                VM2["VM: app-vm-1"]
            end
            subgraph SUBNET_SVC["Subnet: appservice (delegation)\n10.0.3.0/24"]
                DELEGATION["Microsoft.Web/serverFarms\nDelegation"]
            end
            PEERING["VNet Peering\n↔ security-vnet"]
        end

        subgraph RG_APP["Resource Group: development-itma-app"]
            ASP["App Service Plan (S1)"]
            AS["App Service\ndevelopment-itma-webapp"]
        end
    end

    subgraph SUB_SEC["Subscription: Security"]
        subgraph RG_SEC["Resource Group: security-rg"]
            VNET_SEC["Virtual Network\n10.1.0.0/16"]
            subgraph SUBNET_SIEM["Subnet: siem\n10.1.0.0/24"]
            end
            subgraph SUBNET_INSP["Subnet: inspect\n10.1.1.0/24"]
            end
        end
    end

    VNET <--> VNET_SEC
    AS --> DELEGATION
    LB --> VM1
    LB --> VM2

    style SUB_DEV fill:#E8F4FD,stroke:#0078D4
    style SUB_SEC fill:#FFF3E0,stroke:#FF8C00
    style VNET fill:#0078D4,color:#fff
    style VNET_SEC fill:#FF8C00,color:#fff
    style LB fill:#2E8B57,color:#fff
    style AS fill:#7B68EE,color:#fff

Remote State Backend Azure

flowchart LR
    subgraph DEVMACHINE["Developer / CI/CD Agent"]
        TF["terraform plan/apply"]
    end

    subgraph AZURE_STORAGE["Azure Storage Account\nitma12345"]
        CONTAINER["Container: terraform-state"]
        BLOB1["networking/terraform.tfstate.development"]
        BLOB2["networking/terraform.tfstate.uat"]
        BLOB3["networking/terraform.tfstate.production"]
        LOCK["*.lock (Locking file)"]
    end

    subgraph AZURE_RESOURCES["Azure Resources"]
        ARM["Azure Resource Manager"]
        INFRA["Deployed Infrastructure\n(VNets, VMs, App Services...)"]
    end

    TF -- "1. Read/Write state\n(SAS Token or MSI)" --> CONTAINER
    CONTAINER --> BLOB1
    CONTAINER --> BLOB2
    CONTAINER --> BLOB3
    TF -- "2. Acquire lock\nduring apply" --> LOCK
    TF -- "3. Call ARM API" --> ARM
    ARM --> INFRA

    style AZURE_STORAGE fill:#E8F4FD,stroke:#0078D4
    style AZURE_RESOURCES fill:#E8F4E8,stroke:#2E8B57
    style DEVMACHINE fill:#F5F5F5,stroke:#666

Azure DevOps CI/CD Pipeline

flowchart TD
    DEV["Developer\nCommit + Push"] --> REPOS["Azure DevOps Repos\n(Git)"]
    REPOS -- "Continuous Deployment Trigger" --> RELEASE["Release created"]

    RELEASE --> STAGE_DEV

    subgraph STAGE_DEV["Stage: Development"]
        D1["Download Artifacts"] --> D2["Install Terraform"]
        D2 --> D3["terraform init\n--backend-config"]
        D3 --> D4["terraform workspace\nselect development"]
        D4 --> D5["terraform plan"]
        D5 --> D6["terraform apply"]
    end

    STAGE_DEV -- "Success" --> STAGE_UAT

    subgraph STAGE_UAT["Stage: UAT"]
        U1["Download Artifacts"] --> U2["Install Terraform"]
        U2 --> U3["terraform init\n--backend-config"]
        U3 --> U4["terraform workspace\nselect uat"]
        U4 --> U5["terraform plan"]
        U5 --> U6["terraform apply"]
    end

    STAGE_UAT -- "Success" --> GATE["🔐 Pre-deployment\nApproval Gate\n(Chris Jones)"]
    GATE -- "Approved" --> STAGE_PROD

    subgraph STAGE_PROD["Stage: Production"]
        P1["Download Artifacts"] --> P2["Install Terraform"]
        P2 --> P3["terraform init\n--backend-config"]
        P3 --> P4["terraform workspace\nselect production"]
        P4 --> P5["terraform plan"]
        P5 --> P6["terraform apply"]
    end

    subgraph VARIABLES["Pipeline Variables (secrets)"]
        V1["ARM_CLIENT_ID"]
        V2["ARM_CLIENT_SECRET"]
        V3["SAS_TOKEN"]
    end

    VARIABLES -.-> STAGE_DEV
    VARIABLES -.-> STAGE_UAT
    VARIABLES -.-> STAGE_PROD

    style GATE fill:#FF8C00,color:#fff
    style STAGE_PROD fill:#DC143C,color:#fff
    style STAGE_DEV fill:#2E8B57,color:#fff
    style STAGE_UAT fill:#4169E1,color:#fff

8. Reference Tables

AzureRM Provider Resources

Terraform ResourceAzure TypeDescription
azurerm_resource_groupResource GroupLogical container for Azure resources
azurerm_virtual_networkVNetIsolated virtual network
azurerm_subnetSubnetSub-network within a VNet
azurerm_network_security_groupNSGNetwork filtering rules
azurerm_subnet_network_security_group_associationNSG AssociationLinks an NSG to a subnet
azurerm_virtual_network_peeringVNet PeeringConnection between two VNets
azurerm_network_interfaceNICNetwork interface for a VM
azurerm_linux_virtual_machineLinux VMLinux virtual machine
azurerm_windows_virtual_machineWindows VMWindows virtual machine
azurerm_lbLoad BalancerLoad balancer (public or internal)
azurerm_lb_backend_address_poolLB Backend PoolGroup of backends for the LB
azurerm_storage_accountStorage AccountAzure storage account
azurerm_storage_containerBlob ContainerContainer within a Storage Account
azurerm_service_planApp Service PlanApp Service hosting plan
azurerm_linux_web_appApp Service (Linux)Linux web application
azurerm_windows_web_appApp Service (Windows)Windows web application
azurerm_resource_group_template_deploymentARM DeploymentARM template deployment
azurerm_role_definitionCustom RoleCustom RBAC role
azurerm_role_assignmentRole AssignmentRBAC role assignment
random_integer(Random provider)Generates a random integer (e.g. unique suffix)

Common AzureRM Data Sources

Data SourceDescription
azurerm_subscriptionInformation about the current subscription
azurerm_resource_groupReference an existing Resource Group
azurerm_virtual_networkReference an existing VNet
azurerm_subnetReference an existing Subnet
azurerm_client_configInformation about the current identity (tenant_id, object_id…)
azurerm_storage_account_sasGenerate a SAS token
terraform_remote_stateConsume the state of another configuration
template_fileRender a template with variables

ARM_* Environment Variables

VariableDescription
ARM_CLIENT_IDService Principal Client ID
ARM_CLIENT_SECRETService Principal secret
ARM_SUBSCRIPTION_IDTarget Azure subscription ID
ARM_TENANT_IDAzure AD tenant ID
ARM_USE_MSItrue to use Managed Service Identity
ARM_ENVIRONMENTpublic, usgovernment, german, china
TF_VAR_<name>Pass a value to a Terraform variable

Authentication Methods

MethodProvider ArgsEnv VariablesScenario
Azure CLI(none)(Azure CLI credentials)Local dev, Cloud Shell
Service Principal (secret)client_id, client_secret, tenant_idARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_IDCI/CD pipelines
Service Principal (certificate)client_certificate_path, client_certificate_passwordHighly secure environments
MSIuse_msi = trueARM_USE_MSI=trueAzure VMs, Azure DevOps self-hosted agents

Supported Backends

BackendLockingWorkspacesNotes
localDefault — local .tfstate file
azurermAzure Blob Storage — recommended for Azure
s3✅ (via DynamoDB)AWS S3
gcsGoogle Cloud Storage
consulHashiCorp Consul
remote (Terraform Cloud)Terraform Cloud / Enterprise
pgPostgreSQL

9. HCL Code Reference Snippets

Typical project structure

terraform-project/
├── main.tf           # Main resources
├── variables.tf      # Variable declarations
├── outputs.tf        # Exported values
├── providers.tf      # Provider configuration
├── backend.tf        # Remote state backend configuration
├── locals.tf         # Local variables (locals block)
├── terraform.tfvars  # Variable values (non-secrets)
└── modules/
    └── networking/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

Complete variables (variables.tf)

# Simple variable with default
variable "location" {
  type        = string
  description = "Azure region for resources"
  default     = "eastus"
}

# Required variable (no default)
variable "resource_group_name" {
  type        = string
  description = "Resource Group name"
}

# List variable
variable "subnet_names" {
  type        = list(string)
  description = "Subnet names"
  default     = ["web", "database", "app"]
}

# Map variable (for workspaces)
variable "vnet_cidr_range" {
  type = map(string)
  default = {
    development = "10.0.0.0/16"
    uat         = "10.2.0.0/16"
    production  = "10.4.0.0/16"
  }
}

# Complex map variable (for remote state config)
variable "network_state" {
  type        = map(string)
  sensitive   = true
  description = "Remote state config: sa, cn, key, sts"
}

Locals block

locals {
  # Combination of workspace + variable
  full_rg_name = "${terraform.workspace}-${var.resource_group_name}"

  # Common tags for all resources
  common_tags = {
    environment = terraform.workspace
    project     = "globomantics"
    managed_by  = "terraform"
    cost_center = "it"
  }
}

Outputs (outputs.tf)

output "vnet_id" {
  value       = module.vnet.vnet_id
  description = "Virtual Network ID"
}

output "subnet_ids" {
  value       = module.vnet.vnet_subnets_name_id
  description = "Map of name -> subnet ID"
}

output "resource_group_name" {
  value = azurerm_resource_group.main.name
}

output "storage_sas_token" {
  value     = data.azurerm_storage_account_sas.state.sas
  sensitive = true  # Hidden in logs
}

Provider with alias (providers.tf)

terraform {
  required_version = ">= 1.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
    azuread = {
      source  = "hashicorp/azuread"
      version = "~> 2.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.0"
    }
  }
}

# Default provider — dev subscription
provider "azurerm" {
  features {}
}

# Security provider — separate subscription, dedicated SP
provider "azurerm" {
  alias           = "security"
  features        {}
  subscription_id = var.sec_sub_id
  client_id       = var.sec_client_id
  client_secret   = var.client_secret
  tenant_id       = var.tenant_id

  skip_provider_registration  = true
  skip_credentials_validation = true
}

# Azure Active Directory
provider "azuread" {
  tenant_id = var.tenant_id
}

Complete VM + Load Balancer deployment

# Application tier — 2 VMs + Internal Load Balancer

# Application Resource Group
resource "azurerm_resource_group" "app" {
  name     = local.full_rg_name
  location = var.location
  tags     = local.common_tags
}

# Network Interfaces
resource "azurerm_network_interface" "app" {
  count               = var.vm_count
  name                = "${local.prefix}-nic-${count.index}"
  resource_group_name = azurerm_resource_group.app.name
  location            = var.location

  ip_configuration {
    name                          = "ipconfig"
    subnet_id                     = data.terraform_remote_state.networking.outputs.subnet_ids["app"]
    private_ip_address_allocation = "Dynamic"
  }

  tags = local.common_tags
}

# NIC -> LB Backend Pool association
resource "azurerm_network_interface_backend_address_pool_association" "app" {
  count                   = var.vm_count
  network_interface_id    = azurerm_network_interface.app[count.index].id
  ip_configuration_name   = "ipconfig"
  backend_address_pool_id = azurerm_lb_backend_address_pool.app.id
}

# Internal Load Balancer
resource "azurerm_lb" "app" {
  name                = "${local.prefix}-lb"
  resource_group_name = azurerm_resource_group.app.name
  location            = var.location
  sku                 = "Standard"

  frontend_ip_configuration {
    name                          = "internal"
    subnet_id                     = data.terraform_remote_state.networking.outputs.subnet_ids["app"]
    private_ip_address_allocation = "Dynamic"
  }

  tags = local.common_tags
}

resource "azurerm_lb_backend_address_pool" "app" {
  name            = "${local.prefix}-backend-pool"
  loadbalancer_id = azurerm_lb.app.id
}

# Linux Virtual Machines
resource "azurerm_linux_virtual_machine" "app" {
  count               = var.vm_count
  name                = "${local.prefix}-vm-${count.index}"
  resource_group_name = azurerm_resource_group.app.name
  location            = var.location
  size                = "Standard_B2s"
  admin_username      = "azureuser"

  network_interface_ids = [azurerm_network_interface.app[count.index].id]

  admin_ssh_key {
    username   = "azureuser"
    public_key = file("~/.ssh/id_rsa.pub")
  }

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

  source_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "18.04-LTS"
    version   = "latest"
  }

  tags = local.common_tags
}

Complete remote state backend

# backend.tf
terraform {
  backend "azurerm" {
    # Values provided via --backend-config=backend-config.txt
    # to avoid hard-coding credentials
  }
}
# backend-config.txt (do not commit with the secret)
storage_account_name = "itma82341"
container_name       = "terraform-state"
key                  = "networking/terraform.tfstate"
sas_token            = "?sv=2020-08-04&ss=bfqt&srt=sco&sp=rwdlacuptfx..."
# Initialize with external backend
terraform init --backend-config=backend-config.txt

# Select/create a workspace
terraform workspace new development
terraform workspace select development

# Plan and apply
terraform plan -out=tfplan
terraform apply tfplan

10. Summary and Next Steps

Course Recap

ModuleTopicKey Points
2AzureRM Provider3 Azure providers (AzureRM, Stack, AD) • Multiple authentication methods • Cloud Shell
3Multiple ProvidersAlias for multi-subscription • Azure AD provider • Cross-subscription VNet peering
4Remote StateAzure Blob Storage (locking + workspaces) • SAS tokens • State migration
5Azure DevOpsIaC fundamentals • Source control • Release pipelines • Multi-env workspaces
6Data Sources & ARMAtomic layering • Remote state as data source • ARM templates as stop-gap

Key Best Practices

  1. Always use Remote State — even for development deployments. Azure Storage is simple to configure and provides locking + workspaces.

  2. Use Terraform workspaces to manage dev/UAT/prod with a single configuration.

  3. Store code in Source Control — Git is essential for IaC. Private repositories are available for free.

  4. Automate deployments — Assumptions made during manual deployments are revealed (and corrected) by automation.

  5. Atomic configurations — Prefer multiple small configurations linked via remote state rather than one large monolithic configuration.

  6. Separate secrets from non-sensitive values — Secrets in pipeline variables (encrypted), common values in terraform.tfvars in source control.

  7. ARM Templates = stop-gap only — Use them temporarily when the AzureRM provider does not yet support a resource, then refactor to native HCL.


Suggested Next Steps

TopicResource
Infrastructure TestingTerraTest — Go framework for testing Terraform
Testing PowerShell/AzurePester — Testing framework for Windows/Azure infrastructure
Provider documentationTerraform Registry — AzureRM
Provider documentationTerraform Registry — AzureAD
Community modulesTerraform Registry — Azure modules
Exercise filesGitHub — ned1313/Implementing-Terraform-on-Microsoft-Azure

Search Terms

terraform · microsoft · azure · infrastructure · ci/cd · devops · provider · remote · state · arm · azurerm · backend · data · templates · architecture · authentication · pipeline · providers · reference · source · sources · variables · vnet

Interested in this course?

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