You need an Azure subscription (Azure free account works). Install the Az module: Install-Module Az.Accounts, Az.Resources, Az.Compute, Az.Storage -Force -Scope CurrentUser. Run Connect-AzAccount to authenticate. All scripts use $ErrorActionPreference = 'Stop' — if any step fails, the error is raised and the script stops rather than continuing in an inconsistent state.
Lab: Azure Resource Automation
Provision and manage Azure resources with the Az module: create resource groups, deploy and tag virtual machines, configure storage accounts, generate inventory reports, and tear down environments cleanly.
This lab covers the full Azure resource lifecycle using PowerShell and the Az module. You will write scripts that provision an environment, verify it, generate a structured inventory, and clean up—the same pattern used in CI/CD pipelines and Azure Automation Runbooks.
🧪 Lab 1 — Provision a Test Environment
Create a complete test environment: resource group, storage account, and then register the configuration for later cleanup.
#Requires -Modules Az.Accounts, Az.Resources, Az.Storage
[CmdletBinding(SupportsShouldProcess)]
param(
[ValidateSet("dev","staging","test")]
[string]$Environment = "test",
[string]$Location = "uksouth",
[string]$Project = "skilly"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Verify we are authenticated
$ctx = Get-AzContext
if (-not $ctx) {
Write-Error "Not authenticated. Run Connect-AzAccount first."
exit 1
}
Write-Host "Using subscription: $($ctx.Subscription.Name) ($($ctx.Subscription.Id))"
# Build consistent names
$suffix = "$Project-$Environment"
$rgName = "rg-$suffix"
$stoName = "st$($Project)$Environment" # storage names: lowercase, no hyphens
$tags = @{ project = $Project; environment = $Environment; owner = $ctx.Account.Id; created = (Get-Date -Format "yyyy-MM-dd") }
Write-Host "`n--- Creating Resource Group ---"
if ($PSCmdlet.ShouldProcess($rgName, "Create resource group in $Location")) {
$rg = New-AzResourceGroup -Name $rgName -Location $Location -Tag $tags -Force
Write-Host "Resource group: $($rg.ResourceGroupName) [$($rg.Location)]"
}
Write-Host "`n--- Creating Storage Account ---"
if ($PSCmdlet.ShouldProcess($stoName, "Create storage account in $rgName")) {
$sto = New-AzStorageAccount `
-ResourceGroupName $rgName `
-Name $stoName `
-Location $Location `
-SkuName "Standard_LRS" `
-Kind "StorageV2" `
-Tag $tags
Write-Host "Storage account: $($sto.StorageAccountName) [$($sto.Sku.Name)]"
# Create a container for test artifacts
$ctx2 = $sto.Context
New-AzStorageContainer -Name "artifacts" -Context $ctx2 -Permission Off | Out-Null
Write-Host "Created 'artifacts' container (private)."
}
# Save environment config for later labs
$config = [PSCustomObject]@{
Environment = $Environment
Location = $Location
ResourceGroup = $rgName
StorageAccount = $stoName
CreatedAt = (Get-Date -Format "o")
Subscription = $ctx.Subscription.Id
}
$config | ConvertTo-Json | Set-Content -Path ".\env-config-$Environment.json" -Encoding UTF8
Write-Host "`nEnvironment config saved to env-config-$Environment.json"
Write-Host "`n✅ Environment provisioned successfully."
🧪 Lab 2 — Upload Artifacts and Generate Inventory
Upload a test blob to the storage account and generate a full inventory report of all resources in the lab resource group.
#Requires -Modules Az.Accounts, Az.Resources, Az.Storage
[CmdletBinding()]
param(
[string]$ConfigFile = ".\env-config-test.json",
[string]$InventoryOutput = ".\inventory.csv"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Load environment config
if (-not (Test-Path $ConfigFile)) { throw "Config not found: $ConfigFile — run Lab 1 first." }
$cfg = Get-Content $ConfigFile | ConvertFrom-Json
Write-Host "Working with: $($cfg.ResourceGroup) in $($cfg.Location)"
# --- UPLOAD TEST ARTIFACT ---
Write-Host "`n--- Uploading Test Blob ---"
$testContent = "Hello from PowerShell lab - $(Get-Date -Format 'o')"
$tempFile = [System.IO.Path]::GetTempFileName() -replace '\.tmp$','.txt'
$testContent | Set-Content -Path $tempFile -Encoding UTF8
$stoCtx = (Get-AzStorageAccount -ResourceGroupName $cfg.ResourceGroup -Name $cfg.StorageAccount).Context
Set-AzStorageBlobContent `
-Container "artifacts" `
-File $tempFile `
-Blob "test/hello.txt" `
-Context $stoCtx `
-Force | Out-Null
Remove-Item $tempFile -Force
Write-Host "Uploaded test blob: artifacts/test/hello.txt"
# Verify by listing container
$blobs = Get-AzStorageBlob -Container "artifacts" -Context $stoCtx
Write-Host "Blobs in artifacts container: $($blobs.Count)"
# --- GENERATE RESOURCE INVENTORY ---
Write-Host "`n--- Generating Resource Inventory ---"
$resources = Get-AzResource -ResourceGroupName $cfg.ResourceGroup
$inventory = $resources | Select-Object `
@{n="Name"; e={$_.Name}},
@{n="Type"; e={$_.ResourceType}},
@{n="Location"; e={$_.Location}},
@{n="Tags"; e={$_.Tags.Keys -join "; "}},
@{n="ResourceId";e={$_.ResourceId}}
$inventory | Format-Table Name, Type, Location, Tags -AutoSize
$inventory | Export-Csv -Path $InventoryOutput -NoTypeInformation
Write-Host "Inventory exported to $InventoryOutput ($($resources.Count) resources)"
🧪 Lab 3 — Tag Compliance Report
A critical governance task: find all resources missing required tags and report (or remediate) them.
#Requires -Modules Az.Accounts, Az.Resources
[CmdletBinding(SupportsShouldProcess)]
param(
[string[]]$RequiredTags = @("project","environment","owner"),
[string]$SubscriptionId = "",
[switch]$Remediate # if specified, applies placeholder tags to non-compliant resources
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if ($SubscriptionId) { Set-AzContext -SubscriptionId $SubscriptionId }
$ctx = Get-AzContext
Write-Host "Subscription: $($ctx.Subscription.Name)"
$allResources = Get-AzResource
Write-Host "Scanning $($allResources.Count) resources for required tags: $($RequiredTags -join ', ')"
$report = $allResources | ForEach-Object {
$missing = $RequiredTags | Where-Object { -not $_.ResourceType -and -not $_.Tags.ContainsKey($_) }
# Fix: check the resource's tags
$resourceTags = $_.Tags ?? @{}
$missingTags = $RequiredTags | Where-Object { -not $resourceTags.ContainsKey($_) }
[PSCustomObject]@{
Name = $_.Name
Type = $_.ResourceType
ResourceGroup= $_.ResourceGroupName
MissingTags = $missingTags -join "; "
IsCompliant = $missingTags.Count -eq 0
ResourceId = $_.ResourceId
}
}
$compliant = ($report | Where-Object IsCompliant).Count
$nonCompliant = ($report | Where-Object { -not $_.IsCompliant }).Count
Write-Host "`nCompliance Summary:"
Write-Host " Compliant: $compliant"
Write-Warning " Non-compliant: $nonCompliant"
$report | Where-Object { -not $_.IsCompliant } |
Select-Object Name, Type, ResourceGroup, MissingTags |
Format-Table -AutoSize
$report | Export-Csv -Path ".\tag-compliance.csv" -NoTypeInformation
Write-Host "Full report saved to tag-compliance.csv"
# Remediate if requested
if ($Remediate) {
$toFix = $report | Where-Object { -not $_.IsCompliant }
foreach ($item in $toFix) {
if ($PSCmdlet.ShouldProcess($item.Name, "Apply placeholder tags: $($item.MissingTags)")) {
$placeholders = @{}
$item.MissingTags.Split(";").Trim() | Where-Object { $_ } | ForEach-Object {
$placeholders[$_] = "NEEDS-REVIEW"
}
Update-AzTag -ResourceId $item.ResourceId -Tag $placeholders -Operation Merge | Out-Null
Write-Verbose "Tagged $($item.Name) with: $($placeholders.Keys -join ', ')"
}
}
Write-Host "Remediation complete."
}
🧪 Lab 4 — Clean Up Environment
Tear down the lab environment cleanly, with confirmation and a summary of what will be removed.
#Requires -Modules Az.Accounts, Az.Resources
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
[Parameter(Mandatory)]
[ValidateSet("dev","staging","test")]
[string]$Environment
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$configFile = ".\env-config-$Environment.json"
if (-not (Test-Path $configFile)) {
Write-Error "Config file not found: $configFile"
exit 1
}
$cfg = Get-Content $configFile | ConvertFrom-Json
$rgName = $cfg.ResourceGroup
# Show what will be deleted
$resources = Get-AzResource -ResourceGroupName $rgName
Write-Host "`nThe following resource group and $($resources.Count) resources will be PERMANENTLY DELETED:"
Write-Host " Resource Group: $rgName"
$resources | ForEach-Object { Write-Host " - $($_.Name) ($($_.ResourceType))" }
if ($PSCmdlet.ShouldProcess($rgName, "Delete resource group and all contained resources")) {
Write-Host "`nDeleting $rgName ..."
Remove-AzResourceGroup -Name $rgName -Force | Out-Null
Remove-Item $configFile -Force
Write-Host "✅ Resource group deleted and config file removed."
} else {
Write-Host "Operation cancelled (use -Confirm:\$false to skip confirmation)."
}
Always run Lab 4 when you finish this exercise. Azure charges begin as soon as resources are provisioned—even a basic storage account and empty resource group costs fractions of a penny per hour, but 20 forgotten test environments across a team accumulate quickly. Set a calendar reminder if you cannot run the cleanup immediately.
🧪 Exercises
- Run Lab 1 with
-WhatIfto preview the provisioning steps without creating resources. - Run Lab 1 for real, then run Lab 2 to upload the test blob and see the inventory CSV.
- Run Lab 3 against your subscription to see the real tag compliance state.
- Write a function that takes a storage account context and lists all blobs with their sizes in MB.
- Run Lab 4 to clean up (always required).
Combine Labs 1–4 into a single script called Invoke-AzureLabLifecycle.ps1 with a -Phase parameter (Create, Report, Cleanup). Each phase calls the corresponding logic. This is the pattern used by IaC teams for ephemeral environment management — create on PR open, report at PR review, clean up on PR merge.