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
- Course Overview
- Module 2 — Using the AzureRM Provider
- Module 3 — Creating Multiple Providers
- Module 4 — Using Azure for Remote State
- Module 5 — Using Azure DevOps
- Module 6 — Using Data Sources and ARM Templates
- Architecture Diagrams
- Reference Tables
- HCL Code Reference Snippets
- 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:
| Topic | Description |
|---|---|
| Azure Providers | Configuration, authentication and usage of the AzureRM and Azure AD providers |
| Multiple Providers | Using multiple instances in the same configuration |
| Remote State | Storing state in Azure Blob Storage |
| Azure DevOps | Automating deployments with CI/CD pipelines |
| Data Sources & ARM Templates | Consuming 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:
| Aspect | ARM Templates | Terraform (HCL) |
|---|---|---|
| Language | JSON | HashiCorp Configuration Language (HCL) |
| Comments | Not natively supported | Supported (#, //, /* */) |
| Readability | Verbose, difficult to read | More human-readable |
| Parameters | parameters (values submitted to the template) | variables |
| Internal variables | variables (values defined within the template) | locals (locals {} block) |
| Resources | resources | resources |
| Functions | JSON functions | Built-in HCL functions |
| External references | Nested templates | Terraform modules |
Common point of confusion: What ARM calls parameters =
variablesin Terraform. What ARM calls variables =localsin 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:
| Provider | HCL Name | Usage |
|---|---|---|
| AzureRM | azurerm | Azure Public Cloud, Azure Gov, Sovereign Clouds — uses the ARM API |
| Azure Stack | azurestack | On-premises extension of Azure (same ARM API, different versions) |
| Azure Active Directory | azuread | Azure 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:
| Method | Description | Recommended Usage |
|---|---|---|
| Azure CLI | az login — uses CLI tokens | Local development, Cloud Shell |
| Service Principal + Client Secret | client_id + client_secret + tenant_id | CI/CD pipelines |
| Service Principal + Client Certificate | Certificate instead of secret | Secure environments |
| Managed Service Identity (MSI) | Identity assigned to an Azure VM | Azure 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:
- Multi-subscription: Each provider instance is specific to a subscription
- 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:
- Securing the state file — Avoid loss on a failing local disk
- Team collaboration — Sharing a single configuration and state file
Advanced features (depending on the backend):
| Feature | Description |
|---|---|
| Locking | State locking during plan, apply, destroy — prevents concurrent modifications |
| Workspaces | Multiple environments (dev, UAT, prod) with the same code, separate state files |
| Versioning | Version 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:
| Method | HCL Argument | Recommendation |
|---|---|---|
| Managed Service Identity | use_msi = true | Azure VMs with assigned MSI |
| SAS Token | sas_token = "..." | Limited access with expiration ✅ Recommended |
| Storage Access Key | access_key = "..." | Full access — not recommended |
| Service Principal | client_id, client_secret | CI/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:
| Principle | Description |
|---|---|
| Software-defined | Infrastructure is defined by code (Terraform, ARM, Ansible…) |
| Reusable | Reusable modules and templates across different contexts |
| Repeatable | Same configuration deployable identically across multiple environments |
| Source Control | Infrastructure code versioned in Git |
| Automated | Automated 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:
| Platform | Type | Notes |
|---|---|---|
| GitHub | Cloud-hosted | Free private repositories |
| Azure DevOps Repos | Cloud-hosted | Integrated into the Azure ecosystem |
| GitLab | Cloud-hosted / Self-hosted | Integrated CI/CD |
| Bitbucket | Cloud-hosted | Integrated with Atlassian (Jira) |
| Self-hosted Git | Self-managed | Full 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:
| Type | Usage |
|---|---|
| Build Pipeline | Compilation, tests, artifact creation |
| Release Pipeline | Deploying 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.tfvarsfile 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:
- Existing templates not yet refactored
- 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 Resource | Azure Type | Description |
|---|---|---|
azurerm_resource_group | Resource Group | Logical container for Azure resources |
azurerm_virtual_network | VNet | Isolated virtual network |
azurerm_subnet | Subnet | Sub-network within a VNet |
azurerm_network_security_group | NSG | Network filtering rules |
azurerm_subnet_network_security_group_association | NSG Association | Links an NSG to a subnet |
azurerm_virtual_network_peering | VNet Peering | Connection between two VNets |
azurerm_network_interface | NIC | Network interface for a VM |
azurerm_linux_virtual_machine | Linux VM | Linux virtual machine |
azurerm_windows_virtual_machine | Windows VM | Windows virtual machine |
azurerm_lb | Load Balancer | Load balancer (public or internal) |
azurerm_lb_backend_address_pool | LB Backend Pool | Group of backends for the LB |
azurerm_storage_account | Storage Account | Azure storage account |
azurerm_storage_container | Blob Container | Container within a Storage Account |
azurerm_service_plan | App Service Plan | App Service hosting plan |
azurerm_linux_web_app | App Service (Linux) | Linux web application |
azurerm_windows_web_app | App Service (Windows) | Windows web application |
azurerm_resource_group_template_deployment | ARM Deployment | ARM template deployment |
azurerm_role_definition | Custom Role | Custom RBAC role |
azurerm_role_assignment | Role Assignment | RBAC role assignment |
random_integer | (Random provider) | Generates a random integer (e.g. unique suffix) |
Common AzureRM Data Sources
| Data Source | Description |
|---|---|
azurerm_subscription | Information about the current subscription |
azurerm_resource_group | Reference an existing Resource Group |
azurerm_virtual_network | Reference an existing VNet |
azurerm_subnet | Reference an existing Subnet |
azurerm_client_config | Information about the current identity (tenant_id, object_id…) |
azurerm_storage_account_sas | Generate a SAS token |
terraform_remote_state | Consume the state of another configuration |
template_file | Render a template with variables |
ARM_* Environment Variables
| Variable | Description |
|---|---|
ARM_CLIENT_ID | Service Principal Client ID |
ARM_CLIENT_SECRET | Service Principal secret |
ARM_SUBSCRIPTION_ID | Target Azure subscription ID |
ARM_TENANT_ID | Azure AD tenant ID |
ARM_USE_MSI | true to use Managed Service Identity |
ARM_ENVIRONMENT | public, usgovernment, german, china |
TF_VAR_<name> | Pass a value to a Terraform variable |
Authentication Methods
| Method | Provider Args | Env Variables | Scenario |
|---|---|---|---|
| Azure CLI | (none) | (Azure CLI credentials) | Local dev, Cloud Shell |
| Service Principal (secret) | client_id, client_secret, tenant_id | ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID | CI/CD pipelines |
| Service Principal (certificate) | client_certificate_path, client_certificate_password | — | Highly secure environments |
| MSI | use_msi = true | ARM_USE_MSI=true | Azure VMs, Azure DevOps self-hosted agents |
Supported Backends
| Backend | Locking | Workspaces | Notes |
|---|---|---|---|
| local | ❌ | ✅ | Default — local .tfstate file |
| azurerm | ✅ | ✅ | Azure Blob Storage — recommended for Azure |
| s3 | ✅ (via DynamoDB) | ✅ | AWS S3 |
| gcs | ✅ | ✅ | Google Cloud Storage |
| consul | ✅ | ✅ | HashiCorp Consul |
| remote (Terraform Cloud) | ✅ | ✅ | Terraform Cloud / Enterprise |
| pg | ✅ | ✅ | PostgreSQL |
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
| Module | Topic | Key Points |
|---|---|---|
| 2 | AzureRM Provider | 3 Azure providers (AzureRM, Stack, AD) • Multiple authentication methods • Cloud Shell |
| 3 | Multiple Providers | Alias for multi-subscription • Azure AD provider • Cross-subscription VNet peering |
| 4 | Remote State | Azure Blob Storage (locking + workspaces) • SAS tokens • State migration |
| 5 | Azure DevOps | IaC fundamentals • Source control • Release pipelines • Multi-env workspaces |
| 6 | Data Sources & ARM | Atomic layering • Remote state as data source • ARM templates as stop-gap |
Key Best Practices
-
Always use Remote State — even for development deployments. Azure Storage is simple to configure and provides locking + workspaces.
-
Use Terraform workspaces to manage dev/UAT/prod with a single configuration.
-
Store code in Source Control — Git is essential for IaC. Private repositories are available for free.
-
Automate deployments — Assumptions made during manual deployments are revealed (and corrected) by automation.
-
Atomic configurations — Prefer multiple small configurations linked via remote state rather than one large monolithic configuration.
-
Separate secrets from non-sensitive values — Secrets in pipeline variables (encrypted), common values in
terraform.tfvarsin source control. -
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
| Topic | Resource |
|---|---|
| Infrastructure Testing | TerraTest — Go framework for testing Terraform |
| Testing PowerShell/Azure | Pester — Testing framework for Windows/Azure infrastructure |
| Provider documentation | Terraform Registry — AzureRM |
| Provider documentation | Terraform Registry — AzureAD |
| Community modules | Terraform Registry — Azure modules |
| Exercise files | GitHub — 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