Automation25 min read

Azure PowerShell

Automate Azure safely with the Az module, reliable scripts, identity patterns, error handling, idempotency, and operational troubleshooting.

Production-aware guide

Review scope, permissions, impact, and rollback before applying changes.

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:

  1. Load no hidden profile dependencies.
  2. Validate parameters and required modules.
  3. Authenticate using an approved identity.
  4. Set and verify tenant and subscription.
  5. Retrieve the target by exact identifier.
  6. Capture current state without secrets.
  7. Calculate whether a change is required.
  8. Support -WhatIf for the change.
  9. Execute with terminating error behavior.
  10. Retry only classified transient failures.
  11. Verify the resulting Azure and application state.
  12. Return a structured result and meaningful exit code.

Troubleshooting decision table

SymptomFirst evidenceLikely causeSafe next step
Cmdlet missingGet-Command and module versionsMissing/wrong Az moduleInstall approved version in current host
Login works, action failsContext and RBAC scopeAuthorization gapIdentify exact action and scope
Wrong resource changedContext and resource IDName ambiguity or stale contextStop and validate subscription plus ID
Interactive-only successHost/version/profile comparisonHidden profile, prompt, identityRun locally with -NoProfile
$null property failureOriginal cmdlet resultLookup returned nothingValidate result before dereference
Slow inventoryAPI-call countOne request per itemUse Resource Graph or bulk query
ThrottlingHTTP status and retry metadataExcess concurrencyReduce concurrency and back off
Secret in logsPipeline and transcriptTracing or object serializationRotate 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 Stop for critical cmdlets.
  • Use try/catch/finally where recovery or context is needed.
  • Make repeat execution safe.
  • Support -WhatIf for 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.

Official references