Azure PowerShell
Azure PowerShell is a collection of PowerShell modules for managing Azure resources. The Az module provides cmdlets for authentication, resources, networking, compute, storage, applications, monitoring, and other Azure services.
Reliable automation is more than a list of commands. A production script should confirm its target, validate input, handle expected failures, avoid exposing secrets, make repeat runs safe, log meaningful actions, and verify the final state.
Who this guide is for
This guide covers:
- Installing and updating PowerShell and the Az module.
- Azure authentication and subscription context.
- Variables, objects, pipelines, functions, and modules.
- Safe Azure resource automation.
- Error handling, logging, retries, and idempotency.
- Service-principal and managed-identity patterns.
- Common Azure PowerShell failures.
PowerShell fundamentals
PowerShell works with .NET objects rather than only plain text. Cmdlets normally follow a Verb-Noun naming pattern such as Get-AzResourceGroup.
$groups = Get-AzResourceGroup
$groups | Select-Object ResourceGroupName, Location
The pipeline passes objects between commands. Select, filter, sort, and export object properties deliberately.
Get-AzResourceGroup |
Where-Object Location -eq 'westeurope' |
Sort-Object ResourceGroupName |
Select-Object ResourceGroupName, Location
Avoid parsing formatted table output. Formatting cmdlets such as Format-Table are for final display, not for passing reusable data to subsequent commands.
Install the Az module
Check PowerShell:
$PSVersionTable.PSVersion
Install Az for the current user:
Install-Module -Name Az -Repository PSGallery -Scope CurrentUser
Inspect installed versions:
Get-InstalledModule -Name Az -AllVersions
Get-Module -Name Az -ListAvailable
Update during a controlled maintenance window:
Update-Module -Name Az
Review Az release notes and upcoming breaking changes before upgrading production automation. Pin or validate the module version used by CI/CD runners.
Authenticate and select context
Interactive sign-in:
Connect-AzAccount
List available subscriptions:
Get-AzSubscription |
Select-Object Name, Id, TenantId, State
Set the target explicitly:
Set-AzContext -SubscriptionId '<subscription-id>'
Get-AzContext | Select-Object Account, Subscription, Tenant
Production scripts should validate subscription and tenant before changing resources:
$ExpectedSubscriptionId = '<subscription-id>'
$context = Get-AzContext
if ($context.Subscription.Id -ne $ExpectedSubscriptionId) {
throw "Wrong Azure subscription. Expected $ExpectedSubscriptionId but found $($context.Subscription.Id)."
}
Use IDs rather than names when identity must be unambiguous.
Non-interactive identity
Prefer managed identity for automation hosted on a compatible Azure resource:
Connect-AzAccount -Identity
Then set and verify the subscription context.
For CI/CD, workload identity federation avoids storing a reusable client secret. If a service-principal secret or certificate is required, retrieve it from the platform credential store and never commit it to a script.
Assign the minimum Azure role at the narrowest practical scope. Authentication proves identity; RBAC determines which operations are allowed.
Variables and secure input
Use descriptive names and typed parameters:
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$SubscriptionId,
[Parameter(Mandatory)]
[ValidatePattern('^rg-[a-zA-Z0-9-]+$')]
[string]$ResourceGroupName,
[ValidateSet('westeurope', 'northeurope')]
[string]$Location = 'westeurope'
)
Do not treat SecureString as complete secret management. Avoid displaying secrets, embedding them in command lines, or storing them in transcripts.
Create a resource group safely
An idempotent pattern checks current state and changes only what is needed:
param(
[Parameter(Mandatory)]
[string]$ResourceGroupName,
[Parameter(Mandatory)]
[string]$Location
)
$lookupParameters = @{
Name = $ResourceGroupName
ErrorAction = 'SilentlyContinue'
}
$existing = Get-AzResourceGroup @lookupParameters
if ($null -eq $existing) {
Write-Information "Creating resource group '$ResourceGroupName' in '$Location'."
$createParameters = @{
Name = $ResourceGroupName
Location = $Location
Tag = @{ managedBy = 'PowerShell'; workload = 'cloudforge-example' }
ErrorAction = 'Stop'
}
New-AzResourceGroup @createParameters
}
elseif ($existing.Location -ne $Location) {
throw "Resource group exists in '$($existing.Location)', not '$Location'."
}
else {
Write-Information "Resource group '$ResourceGroupName' already exists."
}
Splatting is also useful when the same arguments are assembled separately:
$parameters = @{
Name = $ResourceGroupName
Location = $Location
Tag = @{ managedBy = 'PowerShell'; workload = 'cloudforge-example' }
ErrorAction = 'Stop'
}
New-AzResourceGroup @parameters
Prefer splatting for long command invocations. It improves readability and avoids fragile continuation syntax.
Functions
Create focused advanced functions with validation and ShouldProcess for changing operations:
function Set-CloudForgeResourceTag {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
param(
[Parameter(Mandatory)]
[string]$ResourceId,
[Parameter(Mandatory)]
[hashtable]$Tag
)
if ($PSCmdlet.ShouldProcess($ResourceId, 'Update Azure resource tags')) {
Update-AzTag -ResourceId $ResourceId -Tag $Tag -Operation Merge -ErrorAction Stop
}
}
Test without applying changes:
Set-CloudForgeResourceTag -ResourceId '<resource-id>' `
-Tag @{ owner = 'platform-team' } `
-WhatIf
Error handling
Many cmdlets report non-terminating errors by default. Use -ErrorAction Stop when a failure must enter catch.
try {
$resourceGroup = Get-AzResourceGroup `
-Name $ResourceGroupName `
-ErrorAction Stop
Write-Information "Found resource group $($resourceGroup.ResourceGroupName)."
}
catch {
Write-Error "Unable to read resource group '$ResourceGroupName': $($_.Exception.Message)"
throw
}
finally {
Write-Verbose 'Resource-group lookup completed.'
}
Preserve the original error by using throw inside catch. Avoid exit deep inside reusable functions; throw an error and let the caller decide how the process should end.
Retry transient operations
Retry only errors likely to be temporary. Do not retry authorization, validation, or policy failures blindly.
function Invoke-WithRetry {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[scriptblock]$Operation,
[ValidateRange(1, 10)]
[int]$MaximumAttempts = 4
)
for ($attempt = 1; $attempt -le $MaximumAttempts; $attempt++) {
try {
return & $Operation
}
catch {
if ($attempt -eq $MaximumAttempts) {
throw
}
$delaySeconds = [Math]::Pow(2, $attempt)
Write-Warning "Attempt $attempt failed. Retrying in $delaySeconds seconds."
Start-Sleep -Seconds $delaySeconds
}
}
}
In production, classify retryable exceptions, add jitter, honor server retry guidance, and cap total execution time.
Logging
Use PowerShell streams intentionally:
Write-Information 'Starting deployment validation.'
Write-Verbose 'Detailed diagnostic message.'
Write-Warning 'A recoverable or risky condition was detected.'
Write-Error 'The requested operation failed.'
Log timestamps, correlation identifiers, target subscription, resource group, operation, and result. Never log access tokens, passwords, connection strings, secret values, or full state objects that may contain them.
For automation, structured JSON logs are easier to search:
[pscustomobject]@{
timestamp = (Get-Date).ToUniversalTime().ToString('o')
operation = 'ResourceGroupValidation'
resourceGroup = $ResourceGroupName
status = 'Succeeded'
} | ConvertTo-Json -Compress
Query Azure resources
List resources in a group:
Get-AzResource -ResourceGroupName '<resource-group>' |
Select-Object Name, ResourceType, Location
Find stopped or deallocated VMs:
Get-AzVM -Status |
Where-Object { $_.PowerState -ne 'VM running' } |
Select-Object Name, ResourceGroupName, PowerState
For inventory across many subscriptions, consider Azure Resource Graph because it is optimized for large cross-resource queries.
App Service diagnostics
Check an app and state:
$app = Get-AzWebApp `
-ResourceGroupName '<resource-group>' `
-Name '<web-app>'
$app | Select-Object Name, State, DefaultHostName, Location
Inspect recent deployment and application telemetry through the appropriate App Service and monitoring cmdlets or Azure CLI/API. A running App Service resource can still return application-level 5xx errors, so verify the health endpoint and logs.
Safe deletion pattern
Deletion must be explicit and protected:
function Remove-CloudForgeResourceGroup {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
[Parameter(Mandatory)]
[string]$ResourceGroupName,
[Parameter(Mandatory)]
[ValidateSet('dev', 'test')]
[string]$Environment
)
$resourceGroup = Get-AzResourceGroup `
-Name $ResourceGroupName `
-ErrorAction Stop
if ($PSCmdlet.ShouldProcess($resourceGroup.ResourceId, 'Delete resource group and all contained resources')) {
Remove-AzResourceGroup `
-Name $ResourceGroupName `
-Force `
-ErrorAction Stop
}
}
This sample intentionally excludes production through ValidateSet, but real safeguards must also validate subscription, tags, resource locks, approvals, backups, and dependencies.
Parallel work
PowerShell supports parallel execution, but Azure APIs enforce throttling and concurrency can increase risk. Use bounded parallelism for independent read operations. Serialize changes that target the same resource or depend on ordering.
Objects, properties, and methods
Use Get-Member when you do not know what a command returned:
$resourceGroup = Get-AzResourceGroup -Name '<resource-group>'
$resourceGroup | Get-Member
$resourceGroup.PSObject.Properties.Name
Select computed properties without converting the object to text:
Get-AzResourceGroup | Select-Object `
ResourceGroupName,
Location,
@{ Name = 'TagCount'; Expression = { $_.Tags.Count } }
Export data only after selecting the required fields:
Get-AzResourceGroup |
Select-Object ResourceGroupName, Location |
Export-Csv -Path './resource-groups.csv' -NoTypeInformation -Encoding utf8
Collections and performance
Avoid repeatedly growing a fixed array with += inside a large loop. Let the pipeline collect function output or use a generic list when mutation is required.
$results = foreach ($group in Get-AzResourceGroup) {
[pscustomobject]@{
Name = $group.ResourceGroupName
Location = $group.Location
TagCount = $group.Tags.Count
}
}
Prefer a server-side query, Resource Graph, or a bulk API over thousands of individual requests.
Modules and reusable automation
A PowerShell module packages functions and related resources. Suggested layout:
CloudForge.Azure/
├── CloudForge.Azure.psd1
├── CloudForge.Azure.psm1
├── Public/
├── Private/
└── Tests/
Export only the functions that form the supported interface. Give public functions approved verbs, comment-based help, typed parameters, examples, and predictable output objects.
Module manifest information should include a semantic version, compatible PowerShell edition, author/owner, required modules, and exported commands. Pin production dependencies where repeatability matters.
Profiles and automation hosts
Profiles customize interactive sessions. Production scripts should not depend on aliases, functions, modules, or variables loaded only from a user's profile.
Use:
pwsh -NoProfile -File ./Deploy-CloudForge.ps1
CI/CD agents should install or restore declared dependencies explicitly. Record the PowerShell and Az versions with the build evidence.
Azure Resource Graph
Resource Graph can query inventory across subscriptions efficiently.
$query = @'
Resources
| where type =~ 'microsoft.web/sites'
| project name, resourceGroup, subscriptionId, location, kind
| order by name asc
'@
Search-AzGraph -Query $query -First 1000
Use pagination when results exceed the requested page. Resource Graph data is designed for inventory and governance queries; use the resource-specific API when you need operational properties not present in the graph.
Invoke Azure REST APIs
When an Az cmdlet does not yet expose a required API feature, Invoke-AzRestMethod can call Azure Resource Manager using the current Az context.
$path = '/subscriptions/<subscription-id>/resourcegroups/<resource-group>?api-version=2024-03-01'
$response = Invoke-AzRestMethod -Method GET -Path $path
$body = $response.Content | ConvertFrom-Json
$body | Select-Object name, location, id
Use a documented API version, validate the path, and never assemble a changing request from untrusted strings. For PUT or PATCH operations, understand the complete resource schema and replacement semantics before sending a body.
Azure Activity Log investigation
The Activity Log records control-plane operations such as resource creation, update, deletion, and authorization failures.
$startTime = (Get-Date).AddHours(-4)
Get-AzActivityLog -StartTime $startTime |
Select-Object EventTimestamp, OperationName, Status, Caller, ResourceId |
Sort-Object EventTimestamp -Descending
Filter by resource group, resource ID, correlation ID, or time window to narrow the investigation. Application request failures normally require application and data-plane logs in addition to the Activity Log.
Application Gateway investigation
Inspect resource configuration before changing listeners, probes, or backend settings:
$gateway = Get-AzApplicationGateway `
-ResourceGroupName '<resource-group>' `
-Name '<application-gateway>'
$gateway | Select-Object Name, Location, OperationalState, ProvisioningState
$gateway.BackendHttpSettingsCollection |
Select-Object Name, Port, Protocol, RequestTimeout
Backend health may be available through the resource-specific command or REST API supported by the installed Az.Network version. Correlate unhealthy backends with probe host, path, status code, TLS validation, DNS, NSG, route, and application health.
Do not change the probe merely to force a healthy status. Confirm why the existing check fails and whether the new check still represents real readiness.
Certificate inventory pattern
Certificate objects returned by Azure services do not always expose the certificate expiry directly. When certificate bytes are available through an authorized process, load only the public certificate and inspect it:
$certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(
'<path-to-public-certificate-file>'
)
[pscustomobject]@{
Subject = $certificate.Subject
Thumbprint = $certificate.Thumbprint
NotBeforeUtc = $certificate.NotBefore.ToUniversalTime()
NotAfterUtc = $certificate.NotAfter.ToUniversalTime()
DaysRemaining = [math]::Floor(($certificate.NotAfter - (Get-Date)).TotalDays)
}
Do not export private keys for inventory. For Key Vault-backed certificates, query authorized certificate metadata or the certificate endpoint rather than assuming a secret reference contains expiry information.
Pester testing
Separate logic from live Azure calls so functions can be tested. Mock resource cmdlets for unit tests, then run controlled integration tests with a dedicated subscription or resource group.
Example intent:
Describe 'Set-CloudForgeResourceTag' {
It 'does not call Update-AzTag when WhatIf is used' {
Mock Update-AzTag
Set-CloudForgeResourceTag `
-ResourceId '/subscriptions/example/resourceGroups/rg-test' `
-Tag @{ owner = 'platform' } `
-WhatIf
Should -Invoke Update-AzTag -Times 0
}
}
Mock syntax depends on the Pester version. Pin the test framework and verify examples against that version.
PSScriptAnalyzer
Run static analysis in CI:
Invoke-ScriptAnalyzer -Path ./scripts -Recurse -Severity Warning,Error
Use a reviewed settings file for project rules. A suppression should identify the exact rule and justification rather than disabling analysis globally.
Complete automation flow
A reliable Azure automation script should:
- Load no hidden profile dependencies.
- Validate parameters and required modules.
- Authenticate using an approved identity.
- Set and verify tenant and subscription.
- Retrieve the target by exact identifier.
- Capture current state without secrets.
- Calculate whether a change is required.
- Support
-WhatIffor the change. - Execute with terminating error behavior.
- Retry only classified transient failures.
- Verify the resulting Azure and application state.
- Return a structured result and meaningful exit code.
Troubleshooting decision table
| Symptom | First evidence | Likely cause | Safe next step |
|---|---|---|---|
| Cmdlet missing | Get-Command and module versions | Missing/wrong Az module | Install approved version in current host |
| Login works, action fails | Context and RBAC scope | Authorization gap | Identify exact action and scope |
| Wrong resource changed | Context and resource ID | Name ambiguity or stale context | Stop and validate subscription plus ID |
| Interactive-only success | Host/version/profile comparison | Hidden profile, prompt, identity | Run locally with -NoProfile |
$null property failure | Original cmdlet result | Lookup returned nothing | Validate result before dereference |
| Slow inventory | API-call count | One request per item | Use Resource Graph or bulk query |
| Throttling | HTTP status and retry metadata | Excess concurrency | Reduce concurrency and back off |
| Secret in logs | Pipeline and transcript | Tracing or object serialization | Rotate secret and remove unsafe logging |
Script quality checklist
- Use
[CmdletBinding()]and typed parameters. - Validate required identifiers and permitted environments.
- Select Azure subscription and tenant explicitly.
- Use
-ErrorAction Stopfor critical cmdlets. - Use
try/catch/finallywhere recovery or context is needed. - Make repeat execution safe.
- Support
-WhatIffor impactful functions. - Use splatting for long parameter lists.
- Emit useful, secret-free logs.
- Return objects from reusable functions rather than formatted text.
- Verify the final Azure state.
- Use PSScriptAnalyzer and automated tests.
- Sign scripts where organizational policy requires it.
Troubleshooting
Connect-AzAccount succeeds but a cmdlet is unauthorized
Authentication succeeded, but the identity lacks the necessary RBAC action or scope. Confirm context and role assignment:
Get-AzContext
Get-AzRoleAssignment -SignInName '<user-principal-name>'
Role lookup methods differ for users, service principals, and managed identities. Do not grant Owner merely to bypass diagnosis.
Wrong subscription receives the command
Set context using the subscription ID and validate it immediately before the change. In parallel scripts, avoid relying on mutable global context; use context-aware parameters where supported.
Cmdlet is not recognized
Check installed modules and command discovery:
Get-Module -Name Az* -ListAvailable
Get-Command -Name Get-AzResourceGroup -All
Install or import the appropriate module, and confirm the running PowerShell session uses the expected module path.
Parameter cannot be found
The script may be using documentation for a different module version. Check:
Get-Command <cmdlet-name> -Syntax
Get-Help <cmdlet-name> -Full
Review Az release notes before modifying production code.
Object reference not set
An expected command may have returned $null. Validate results before reading nested properties and make lookup failures terminating when appropriate.
A loop is unexpectedly slow
Avoid making one API call per object when a server-side query or Resource Graph can return the collection. Cache invariant lookups and use bounded concurrency only when safe.
Output is truncated or hard to parse
Do not parse formatted tables. Select properties and serialize objects:
$result | Select-Object Name, Id, Location | ConvertTo-Json -Depth 5
Script works interactively but fails in a pipeline
Compare PowerShell edition and version, Az module versions, identity, environment variables, working directory, execution policy, network/proxy settings, and non-interactive prompts.
Command quick reference
Connect-AzAccount
Get-AzContext
Get-AzSubscription
Set-AzContext -SubscriptionId '<subscription-id>'
Get-AzResourceGroup
Get-AzResource -ResourceGroupName '<resource-group>'
Get-AzVM -Status
Get-AzWebApp -ResourceGroupName '<resource-group>' -Name '<web-app>'
Get-AzActivityLog -StartTime (Get-Date).AddHours(-1)
Get-Help <cmdlet-name> -Full
Get-Command <cmdlet-name> -Syntax
Frequently asked questions
Windows PowerShell or PowerShell 7?
Use a currently supported PowerShell version compatible with the required Az modules and operating environment. Test legacy scripts before migration because runtime behavior and modules may differ.
Why should scripts use subscription IDs?
Names may be duplicated or changed. IDs give the script an unambiguous target.
Is -Force safe?
No. It often suppresses confirmation rather than reducing impact. Validate the target and use approvals, ShouldProcess, resource locks, and recovery planning.
How should secrets be passed to scripts?
Use managed identity, workload identity federation, or a protected credential/secret store. Avoid command-line arguments and plain-text files.