🎁 New User? Get 20% off your first purchase with code NEWUSER20 Register Now →
Menu

Categories

PowerShell for Azure Administration 2026: The Complete Automation Guide

PowerShell for Azure Administration 2026: The Complete Automation Guide

PowerShell is the most powerful automation tool for Azure. While the Azure Portal is great for learning and one-off tasks, any serious Azure administrator knows that real efficiency comes from automation. PowerShell lets you manage hundreds of resources in seconds, enforce compliance at scale, and build repeatable infrastructure that doesn't depend on clicking buttons.

In 2026, Azure PowerShell (the Az module) is more capable than ever. With over 6,000+ cmdlets covering every Azure service, cross-platform support (Windows, Linux, macOS), and tight integration with Azure Automation, Azure Functions, and CI/CD pipelines — PowerShell is the essential skill for any Azure professional.

Why PowerShell over Azure CLI? Both are excellent tools, but PowerShell excels at complex automation with its object pipeline, error handling (try/catch), advanced scripting capabilities, and native integration with .NET. Azure CLI is great for quick commands; PowerShell is built for production automation.


Getting Started: Installation & Setup

Install Azure PowerShell (Az Module)

# Install the Az module on any platform

# Check current PowerShell version (7.4+ recommended)
$PSVersionTable.PSVersion

# Install Az module (Windows, Linux, macOS)
Install-Module -Name Az -Repository PSGallery -Force -AllowClobber

# Or update to latest version
Update-Module -Name Az -Force

# Verify installation
Get-Module -Name Az -ListAvailable | Select-Object Name, Version

# Import the module
Import-Module Az

# Check available Az sub-modules
Get-Module -Name Az.* -ListAvailable | 
    Select-Object Name, Version | 
    Sort-Object Name

Authentication Methods

Method Use Case Security Best For
Interactive LoginConnect-AzAccount (browser popup)HighManual administration, learning
Service PrincipalApp registration + client secret/certHighCI/CD pipelines, automation scripts
Managed IdentityAzure-assigned identity (no credentials)HighestAzure VMs, Functions, App Service
Device CodeCode-based auth for headless systemsMediumSSH sessions, remote servers
Access TokenPre-obtained OAuth tokenMediumShort-lived scripts, testing

# Authentication examples

# Method 1: Interactive login (opens browser)
Connect-AzAccount

# Method 2: Service Principal (CI/CD pipelines)
$securePassword = ConvertTo-SecureString $env:AZURE_CLIENT_SECRET -AsPlainText -Force
$credential = New-Object PSCredential($env:AZURE_CLIENT_ID, $securePassword)
Connect-AzAccount -ServicePrincipal -Credential $credential -TenantId $env:AZURE_TENANT_ID

# Method 3: Managed Identity (from Azure VM or Function)
Connect-AzAccount -Identity

# Method 4: Device Code (headless/SSH)
Connect-AzAccount -UseDeviceAuthentication

# Verify connection
Get-AzContext | Select-Object Account, Subscription, Tenant

# Switch subscription
Set-AzContext -SubscriptionId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

# List all accessible subscriptions
Get-AzSubscription | Format-Table Name, Id, State

Virtual Machine Management

Create, Configure, and Manage VMs

# Complete VM lifecycle management

# Create a Resource Group
New-AzResourceGroup -Name "prod-rg" -Location "westeurope"

# Create a Linux VM with SSH key
$vmParams = @{
    ResourceGroupName = "prod-rg"
    Name              = "web-server-01"
    Location          = "westeurope"
    Image             = "Ubuntu2204"
    Size              = "Standard_B2s"
    AdminUsername      = "azureadmin"
    GenerateSshKey     = $true
    SecurityType       = "TrustedLaunch"
    PublicIpSku        = "Standard"
    OpenPorts          = @(22, 80, 443)
}
$vm = New-AzVM @vmParams

# Get VM details
Get-AzVM -ResourceGroupName "prod-rg" -Name "web-server-01" -Status |
    Select-Object Name, @{N='Power';E={$_.Statuses[1].DisplayStatus}},
    @{N='Size';E={$_.HardwareProfile.VmSize}}

# Resize a VM
$vm = Get-AzVM -ResourceGroupName "prod-rg" -Name "web-server-01"
$vm.HardwareProfile.VmSize = "Standard_B4ms"
Update-AzVM -ResourceGroupName "prod-rg" -VM $vm

# Stop VM (deallocate to stop billing)
Stop-AzVM -ResourceGroupName "prod-rg" -Name "web-server-01" -Force

# Start VM
Start-AzVM -ResourceGroupName "prod-rg" -Name "web-server-01"

# Restart VM
Restart-AzVM -ResourceGroupName "prod-rg" -Name "web-server-01"

Bulk VM Operations

# Manage multiple VMs efficiently

# List all VMs across all resource groups with status
Get-AzVM -Status | Select-Object Name, ResourceGroupName,
    @{N='Status';E={$_.PowerState}},
    @{N='Size';E={$_.HardwareProfile.VmSize}},
    Location | Format-Table -AutoSize

# Stop all dev VMs (save costs at night)
Get-AzVM -Status | Where-Object {
    $_.Name -like "dev-*" -and $_.PowerState -eq "VM running"
} | ForEach-Object {
    Write-Host "Stopping $($_.Name)..." -ForegroundColor Yellow
    Stop-AzVM -ResourceGroupName $_.ResourceGroupName -Name $_.Name -Force -AsJob
}

# Tag all untagged VMs with environment label
Get-AzVM | Where-Object { -not $_.Tags.ContainsKey('Environment') } |
    ForEach-Object {
        $tags = $_.Tags ?? @{}
        $tags['Environment'] = 'untagged'
        $tags['ReviewDate'] = (Get-Date).ToString('yyyy-MM-dd')
        Update-AzVM -ResourceGroupName $_.ResourceGroupName -VM $_ -Tag $tags
    }

# Find VMs without auto-shutdown configured
Get-AzVM | ForEach-Object {
    $shutdown = Get-AzResource -ResourceId "$($_.Id)/providers/Microsoft.DevTestLab/schedules/shutdown-computevm-$($_.Name)" -ErrorAction SilentlyContinue
    if (-not $shutdown) {
        [PSCustomObject]@{
            VM       = $_.Name
            RG       = $_.ResourceGroupName
            Size     = $_.HardwareProfile.VmSize
            Warning  = "No auto-shutdown configured!"
        }
    }
} | Format-Table -AutoSize

Networking with PowerShell

# Build a complete network infrastructure

# Create Virtual Network with multiple subnets
$webSubnet = New-AzVirtualNetworkSubnetConfig -Name "web-subnet" -AddressPrefix "10.0.1.0/24"
$appSubnet = New-AzVirtualNetworkSubnetConfig -Name "app-subnet" -AddressPrefix "10.0.2.0/24"
$dbSubnet  = New-AzVirtualNetworkSubnetConfig -Name "db-subnet"  -AddressPrefix "10.0.3.0/24"

$vnet = New-AzVirtualNetwork `
    -ResourceGroupName "prod-rg" `
    -Location "westeurope" `
    -Name "prod-vnet" `
    -AddressPrefix "10.0.0.0/16" `
    -Subnet $webSubnet, $appSubnet, $dbSubnet

# Create Network Security Group with rules
$rule1 = New-AzNetworkSecurityRuleConfig -Name "AllowHTTPS" `
    -Protocol Tcp -Direction Inbound -Priority 100 `
    -SourceAddressPrefix Internet -SourcePortRange * `
    -DestinationAddressPrefix * -DestinationPortRange 443 `
    -Access Allow

$rule2 = New-AzNetworkSecurityRuleConfig -Name "AllowSSH" `
    -Protocol Tcp -Direction Inbound -Priority 110 `
    -SourceAddressPrefix "203.0.113.0/24" -SourcePortRange * `
    -DestinationAddressPrefix * -DestinationPortRange 22 `
    -Access Allow

$rule3 = New-AzNetworkSecurityRuleConfig -Name "DenyAllInbound" `
    -Protocol * -Direction Inbound -Priority 4096 `
    -SourceAddressPrefix * -SourcePortRange * `
    -DestinationAddressPrefix * -DestinationPortRange * `
    -Access Deny

$nsg = New-AzNetworkSecurityGroup `
    -ResourceGroupName "prod-rg" `
    -Location "westeurope" `
    -Name "web-nsg" `
    -SecurityRules $rule1, $rule2, $rule3

# Attach NSG to subnet
$vnet = Get-AzVirtualNetwork -Name "prod-vnet" -ResourceGroupName "prod-rg"
$subnet = Get-AzVirtualNetworkSubnetConfig -Name "web-subnet" -VirtualNetwork $vnet
$subnet.NetworkSecurityGroup = $nsg
Set-AzVirtualNetwork -VirtualNetwork $vnet

# Audit all NSG rules across your subscription
Get-AzNetworkSecurityGroup | ForEach-Object {
    $nsgName = $_.Name
    $_.SecurityRules | Where-Object { $_.SourceAddressPrefix -eq "*" -and $_.Access -eq "Allow" } |
        Select-Object @{N='NSG';E={$nsgName}}, Name, DestinationPortRange, Priority
} | Format-Table -AutoSize

Storage Management

# Azure Storage account and blob management

# Create Storage Account
$storageAccount = New-AzStorageAccount `
    -ResourceGroupName "prod-rg" `
    -Name "prodstorage2026" `
    -Location "westeurope" `
    -SkuName "Standard_LRS" `
    -Kind "StorageV2" `
    -MinimumTlsVersion "TLS1_2" `
    -AllowBlobPublicAccess $false `
    -EnableHttpsTrafficOnly $true

# Get storage context
$ctx = $storageAccount.Context

# Create blob containers
New-AzStorageContainer -Name "backups" -Context $ctx -Permission Off
New-AzStorageContainer -Name "uploads" -Context $ctx -Permission Off
New-AzStorageContainer -Name "logs" -Context $ctx -Permission Off

# Upload a file to blob storage
Set-AzStorageBlobContent `
    -File "./backup-2026-02-18.tar.gz" `
    -Container "backups" `
    -Blob "daily/backup-2026-02-18.tar.gz" `
    -Context $ctx `
    -StandardBlobTier Hot

# Upload entire directory recursively
Get-ChildItem -Path "./website/assets" -Recurse -File | ForEach-Object {
    $blobPath = $_.FullName.Replace("$PWD/website/assets/", "")
    Set-AzStorageBlobContent -File $_.FullName -Container "uploads" `
        -Blob $blobPath -Context $ctx -Force
}

# List blobs with sizes
Get-AzStorageBlob -Container "backups" -Context $ctx |
    Select-Object Name, Length,
    @{N='SizeMB';E={[math]::Round($_.Length/1MB, 2)}},
    LastModified | Format-Table -AutoSize

# Move old blobs to Cool/Archive tier (cost optimization)
Get-AzStorageBlob -Container "backups" -Context $ctx | Where-Object {
    $_.LastModified -lt (Get-Date).AddDays(-30)
} | ForEach-Object {
    $_.BlobClient.SetAccessTier("Cool")
    Write-Host "Moved $($_.Name) to Cool tier" -ForegroundColor Cyan
}

# Generate SAS token for secure temporary access
$sasToken = New-AzStorageBlobSASToken `
    -Container "backups" `
    -Blob "daily/backup-2026-02-18.tar.gz" `
    -Permission r `
    -ExpiryTime (Get-Date).AddHours(4) `
    -Context $ctx
Write-Host "Download URL: $($storageAccount.PrimaryEndpoints.Blob)backups/daily/backup-2026-02-18.tar.gz$sasToken"

Azure Key Vault — Secrets Management

# Secure secrets, keys, and certificates with Key Vault

# Create Key Vault
New-AzKeyVault `
    -VaultName "prod-keyvault-2026" `
    -ResourceGroupName "prod-rg" `
    -Location "westeurope" `
    -EnableRbacAuthorization `
    -EnablePurgeProtection `
    -SoftDeleteRetentionInDays 90

# Store a secret
$secretValue = ConvertTo-SecureString "MyS3cur3P@ssw0rd!" -AsPlainText -Force
Set-AzKeyVaultSecret -VaultName "prod-keyvault-2026" `
    -Name "DatabasePassword" `
    -SecretValue $secretValue `
    -Tag @{ Environment = "Production"; Application = "WebApp" }

# Retrieve a secret
$secret = Get-AzKeyVaultSecret -VaultName "prod-keyvault-2026" -Name "DatabasePassword"
$plaintext = $secret.SecretValue | ConvertFrom-SecureString -AsPlainText

# List all secrets with expiry dates
Get-AzKeyVaultSecret -VaultName "prod-keyvault-2026" | Select-Object Name, Enabled,
    @{N='Expires';E={$_.Attributes.Expires}},
    @{N='DaysUntilExpiry';E={
        if ($_.Attributes.Expires) { ($_.Attributes.Expires - (Get-Date)).Days }
        else { "No expiry set" }
    }} | Format-Table -AutoSize

# Rotate secrets automatically
function Rotate-DatabasePassword {
    param([string]$VaultName, [string]$SecretName)
    
    $newPassword = -join ((65..90) + (97..122) + (48..57) + (33,35,36,37) | 
        Get-Random -Count 24 | ForEach-Object { [char]$_ })
    
    $securePassword = ConvertTo-SecureString $newPassword -AsPlainText -Force
    Set-AzKeyVaultSecret -VaultName $VaultName -Name $SecretName `
        -SecretValue $securePassword `
        -Expires (Get-Date).AddDays(90) `
        -Tag @{ RotatedOn = (Get-Date).ToString('yyyy-MM-dd') }
    
    Write-Host "Secret '$SecretName' rotated. New expiry: $((Get-Date).AddDays(90))" -ForegroundColor Green
}

# Usage
Rotate-DatabasePassword -VaultName "prod-keyvault-2026" -SecretName "DatabasePassword"

Security Rule: Never store secrets in your PowerShell scripts, environment variables, or source code. Always use Key Vault with Managed Identity or Service Principal authentication. This is non-negotiable in production environments.


RBAC & Identity Management

# Role-Based Access Control management

# List all role assignments for a resource group
Get-AzRoleAssignment -ResourceGroupName "prod-rg" |
    Select-Object DisplayName, RoleDefinitionName, Scope, ObjectType |
    Format-Table -AutoSize

# Assign Reader role to a user
New-AzRoleAssignment `
    -SignInName "john@company.com" `
    -RoleDefinitionName "Reader" `
    -ResourceGroupName "prod-rg"

# Assign custom role at subscription level
New-AzRoleAssignment `
    -ObjectId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
    -RoleDefinitionName "Contributor" `
    -Scope "/subscriptions/$((Get-AzContext).Subscription.Id)"

# Remove a role assignment
Remove-AzRoleAssignment `
    -SignInName "john@company.com" `
    -RoleDefinitionName "Contributor" `
    -ResourceGroupName "prod-rg"

# Audit: Find all Owner/Contributor assignments (security review)
Get-AzRoleAssignment | Where-Object {
    $_.RoleDefinitionName -in @("Owner", "Contributor")
} | Select-Object DisplayName, RoleDefinitionName, Scope, ObjectType |
    Sort-Object RoleDefinitionName | Format-Table -AutoSize

# Create a custom role
$role = @{
    Name        = "VM Operator"
    Description = "Can start, stop, and restart VMs but not create or delete them"
    Actions     = @(
        "Microsoft.Compute/virtualMachines/start/action",
        "Microsoft.Compute/virtualMachines/powerOff/action",
        "Microsoft.Compute/virtualMachines/restart/action",
        "Microsoft.Compute/virtualMachines/read",
        "Microsoft.Resources/subscriptions/resourceGroups/read"
    )
    NotActions    = @()
    AssignableScopes = @("/subscriptions/$((Get-AzContext).Subscription.Id)")
}
New-AzRoleDefinition -Role $role

Azure SQL Database Management

# Manage Azure SQL databases with PowerShell

# Create SQL Server
$adminPassword = ConvertTo-SecureString "Str0ngP@ssw0rd!" -AsPlainText -Force
$adminCredential = New-Object PSCredential("sqladmin", $adminPassword)

New-AzSqlServer `
    -ResourceGroupName "prod-rg" `
    -ServerName "prod-sql-2026" `
    -Location "westeurope" `
    -SqlAdministratorCredentials $adminCredential `
    -MinimalTlsVersion "1.2"

# Create database
New-AzSqlDatabase `
    -ResourceGroupName "prod-rg" `
    -ServerName "prod-sql-2026" `
    -DatabaseName "webapp-db" `
    -Edition "Standard" `
    -RequestedServiceObjectiveName "S1" `
    -MaxSizeBytes 268435456000  # 250 GB

# Configure firewall rules
New-AzSqlServerFirewallRule `
    -ResourceGroupName "prod-rg" `
    -ServerName "prod-sql-2026" `
    -FirewallRuleName "AllowAzureServices" `
    -StartIpAddress "0.0.0.0" `
    -EndIpAddress "0.0.0.0"

# Enable auditing
Set-AzSqlDatabaseAudit `
    -ResourceGroupName "prod-rg" `
    -ServerName "prod-sql-2026" `
    -DatabaseName "webapp-db" `
    -State Enabled `
    -StorageAccountResourceId "/subscriptions/.../storageAccounts/prodstorage2026"

# Scale database (upgrade performance tier)
Set-AzSqlDatabase `
    -ResourceGroupName "prod-rg" `
    -ServerName "prod-sql-2026" `
    -DatabaseName "webapp-db" `
    -RequestedServiceObjectiveName "S2"  # Scale up

# Export database backup
$exportRequest = New-AzSqlDatabaseExport `
    -ResourceGroupName "prod-rg" `
    -ServerName "prod-sql-2026" `
    -DatabaseName "webapp-db" `
    -StorageKeyType "StorageAccessKey" `
    -StorageKey (Get-AzStorageAccountKey -ResourceGroupName "prod-rg" -Name "prodstorage2026")[0].Value `
    -StorageUri "https://prodstorage2026.blob.core.windows.net/backups/webapp-db.bacpac" `
    -AdministratorLogin "sqladmin" `
    -AdministratorLoginPassword $adminPassword

Cost Management & Optimization Scripts

# Production-ready cost management automation

# Get spending for current billing period
$startDate = (Get-Date -Day 1).ToString("yyyy-MM-dd")
$endDate = (Get-Date).ToString("yyyy-MM-dd")

$costs = Get-AzConsumptionUsageDetail -StartDate $startDate -EndDate $endDate

# Top 20 most expensive resources
$costs | Group-Object InstanceName |
    Select-Object Name, Count,
    @{N='TotalCost';E={[math]::Round(($_.Group | Measure-Object PretaxCost -Sum).Sum, 2)}} |
    Sort-Object TotalCost -Descending |
    Select-Object -First 20 | Format-Table -AutoSize

# Cost by service category
$costs | Group-Object ConsumedService |
    Select-Object Name,
    @{N='Cost';E={[math]::Round(($_.Group | Measure-Object PretaxCost -Sum).Sum, 2)}} |
    Sort-Object Cost -Descending | Format-Table -AutoSize

# Find orphaned (unattached) resources — money wasters!
Write-Host "`n=== ORPHANED DISKS ===" -ForegroundColor Red
Get-AzDisk | Where-Object { $_.ManagedBy -eq $null } |
    Select-Object Name, DiskSizeGB, Location,
    @{N='MonthlyCost';E={[math]::Round($_.DiskSizeGB * 0.04, 2)}} |
    Format-Table -AutoSize

Write-Host "`n=== ORPHANED PUBLIC IPs ===" -ForegroundColor Red
Get-AzPublicIpAddress | Where-Object { $_.IpConfiguration -eq $null } |
    Select-Object Name, Location, PublicIpAllocationMethod,
    @{N='MonthlyCost';E={"~`$3.65"}} | Format-Table -AutoSize

Write-Host "`n=== ORPHANED NICs ===" -ForegroundColor Red
Get-AzNetworkInterface | Where-Object { $_.VirtualMachine -eq $null } |
    Select-Object Name, Location, ResourceGroupName | Format-Table -AutoSize

# Calculate total waste
$orphanedDisks = Get-AzDisk | Where-Object { $_.ManagedBy -eq $null }
$diskWaste = ($orphanedDisks | Measure-Object DiskSizeGB -Sum).Sum * 0.04
$ipWaste = (Get-AzPublicIpAddress | Where-Object { $_.IpConfiguration -eq $null }).Count * 3.65
$totalWaste = [math]::Round($diskWaste + $ipWaste, 2)
Write-Host "`nTotal Monthly Waste: `$$totalWaste" -ForegroundColor Red -BackgroundColor Black

Azure Advisor Recommendations

# Get Azure Advisor cost recommendations
Get-AzAdvisorRecommendation -Category Cost |
    Select-Object @{N='Impact';E={$_.Impact}},
    @{N='Problem';E={$_.ShortDescription.Problem}},
    @{N='Solution';E={$_.ShortDescription.Solution}},
    @{N='Resource';E={$_.ResourceMetadata.ResourceId.Split('/')[-1]}} |
    Format-Table -AutoSize -Wrap

# Get all recommendation categories
foreach ($category in @("Cost", "Security", "Performance", "HighAvailability", "OperationalExcellence")) {
    $recs = Get-AzAdvisorRecommendation -Category $category
    Write-Host "$category: $($recs.Count) recommendations" -ForegroundColor $(
        if ($recs.Count -gt 5) { "Red" } elseif ($recs.Count -gt 0) { "Yellow" } else { "Green" }
    )
}

Azure Policy & Governance

# Enforce governance at scale with Azure Policy

# List all policy assignments
Get-AzPolicyAssignment | Select-Object Name, 
    @{N='Scope';E={$_.Properties.Scope}},
    @{N='Policy';E={$_.Properties.DisplayName}} |
    Format-Table -AutoSize

# Assign built-in policy: "Require a tag on resources"
$policy = Get-AzPolicyDefinition | Where-Object {
    $_.Properties.DisplayName -eq "Require a tag on resources"
}

New-AzPolicyAssignment `
    -Name "RequireEnvironmentTag" `
    -DisplayName "Require Environment Tag" `
    -PolicyDefinition $policy `
    -Scope "/subscriptions/$((Get-AzContext).Subscription.Id)" `
    -PolicyParameterObject @{ tagName = @{ value = "Environment" } }

# Check compliance state
Get-AzPolicyState -SubscriptionId (Get-AzContext).Subscription.Id |
    Group-Object ComplianceState |
    Select-Object Name, Count | Format-Table -AutoSize

# Find non-compliant resources
Get-AzPolicyState -Filter "ComplianceState eq 'NonCompliant'" |
    Select-Object ResourceId, PolicyAssignmentName, ComplianceState |
    Format-Table -AutoSize

Monitoring & Alerting

# Set up monitoring and alerts with PowerShell

# Create an Action Group (email notification)
$emailReceiver = New-AzActionGroupReceiver `
    -Name "AdminEmail" `
    -EmailReceiver `
    -EmailAddress "admin@company.com"

$actionGroup = Set-AzActionGroup `
    -ResourceGroupName "prod-rg" `
    -Name "CriticalAlerts" `
    -ShortName "Critical" `
    -Receiver $emailReceiver

# Create metric alert: VM CPU > 90% for 5 minutes
$condition = New-AzMetricAlertRuleV2Criteria `
    -MetricName "Percentage CPU" `
    -Operator GreaterThan `
    -Threshold 90 `
    -TimeAggregation Average

Add-AzMetricAlertRuleV2 `
    -Name "HighCPU-WebServer" `
    -ResourceGroupName "prod-rg" `
    -WindowSize 00:05:00 `
    -Frequency 00:01:00 `
    -TargetResourceId "/subscriptions/.../virtualMachines/web-server-01" `
    -Condition $condition `
    -ActionGroupId $actionGroup.Id `
    -Severity 2 `
    -Description "Alert when CPU exceeds 90% for 5 minutes"

# Query Log Analytics with KQL
$workspace = Get-AzOperationalInsightsWorkspace -ResourceGroupName "prod-rg"
$query = @"
Heartbeat
| summarize LastHeartbeat = max(TimeGenerated) by Computer
| where LastHeartbeat < ago(5m)
| project Computer, LastHeartbeat, MinutesAgo = datetime_diff('minute', now(), LastHeartbeat)
"@

$results = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspace.CustomerId -Query $query
$results.Results | Format-Table -AutoSize

Azure Functions with PowerShell

Serverless PowerShell: Azure Functions supports PowerShell 7.4 as a runtime. This means you can run your automation scripts as serverless functions — triggered by HTTP requests, timers, queue messages, or blob events — without managing any servers.

# Example: Azure Function — Daily Cost Report (timer trigger)

# run.ps1 — Azure Function with Timer Trigger
param($Timer)

# Authenticate with Managed Identity
Connect-AzAccount -Identity

$startDate = (Get-Date).AddDays(-1).ToString("yyyy-MM-dd")
$endDate = (Get-Date).ToString("yyyy-MM-dd")

# Get yesterday's costs
$costs = Get-AzConsumptionUsageDetail -StartDate $startDate -EndDate $endDate
$totalCost = [math]::Round(($costs | Measure-Object PretaxCost -Sum).Sum, 2)

# Top 5 most expensive resources
$topResources = $costs | Group-Object InstanceName |
    Select-Object Name, @{N='Cost';E={[math]::Round(($_.Group | Measure-Object PretaxCost -Sum).Sum, 2)}} |
    Sort-Object Cost -Descending | Select-Object -First 5

# Build report
$report = @"
Azure Daily Cost Report - $startDate
=====================================
Total Spend: `$$totalCost

Top 5 Resources:
$($topResources | ForEach-Object { "  - $($_.Name): `$$($_.Cost)" } | Out-String)
"@

# Send via SendGrid (output binding) or Teams webhook
$body = @{
    text = $report
} | ConvertTo-Json

Invoke-RestMethod -Uri $env:TEAMS_WEBHOOK_URL -Method Post -Body $body -ContentType "application/json"

Write-Host "Cost report sent. Total: `$$totalCost"

Production Automation Patterns

Pattern 1: Automated Disaster Recovery

# Automated VM backup and cross-region replication
function Invoke-DisasterRecoveryBackup {
    param(
        [string]$SourceRG = "prod-rg",
        [string]$TargetRG = "dr-rg",
        [string]$TargetLocation = "northeurope"
    )

    $timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
    
    # Snapshot all VM disks
    Get-AzVM -ResourceGroupName $SourceRG | ForEach-Object {
        $vm = $_
        $osDisk = Get-AzDisk -ResourceGroupName $SourceRG -DiskName $vm.StorageProfile.OsDisk.Name
        
        $snapshotConfig = New-AzSnapshotConfig `
            -SourceUri $osDisk.Id `
            -Location $vm.Location `
            -CreateOption Copy
        
        $snapshotName = "$($vm.Name)-snapshot-$timestamp"
        New-AzSnapshot -ResourceGroupName $SourceRG -SnapshotName $snapshotName -Snapshot $snapshotConfig
        
        Write-Host "Created snapshot: $snapshotName" -ForegroundColor Green
    }

    # Copy snapshots to DR region
    Get-AzSnapshot -ResourceGroupName $SourceRG | Where-Object { $_.Name -like "*$timestamp*" } |
        ForEach-Object {
            $drSnapshotConfig = New-AzSnapshotConfig `
                -SourceResourceId $_.Id `
                -Location $TargetLocation `
                -CreateOption CopyStart
            
            New-AzSnapshot -ResourceGroupName $TargetRG `
                -SnapshotName "dr-$($_.Name)" -Snapshot $drSnapshotConfig -AsJob
        }
    
    Write-Host "DR backup completed. Snapshots replicating to $TargetLocation" -ForegroundColor Cyan
}

Pattern 2: Infrastructure Compliance Audit

# Comprehensive security audit script
function Invoke-AzureSecurityAudit {
    $report = [System.Collections.ArrayList]@()

    # Check 1: VMs without encryption
    Get-AzVM | ForEach-Object {
        $diskEncryption = Get-AzVMDiskEncryptionStatus -ResourceGroupName $_.ResourceGroupName -VMName $_.Name
        if ($diskEncryption.OsVolumeEncrypted -ne "Encrypted") {
            $report.Add([PSCustomObject]@{
                Check    = "Disk Encryption"
                Severity = "HIGH"
                Resource = $_.Name
                Issue    = "OS disk is not encrypted"
            }) | Out-Null
        }
    }

    # Check 2: Storage accounts without HTTPS enforcement
    Get-AzStorageAccount | Where-Object { -not $_.EnableHttpsTrafficOnly } |
        ForEach-Object {
            $report.Add([PSCustomObject]@{
                Check    = "HTTPS Only"
                Severity = "HIGH"
                Resource = $_.StorageAccountName
                Issue    = "HTTP traffic allowed (should be HTTPS only)"
            }) | Out-Null
        }

    # Check 3: NSGs with 0.0.0.0/0 inbound rules
    Get-AzNetworkSecurityGroup | ForEach-Object {
        $_.SecurityRules | Where-Object {
            $_.SourceAddressPrefix -eq "*" -and 
            $_.Access -eq "Allow" -and 
            $_.Direction -eq "Inbound" -and
            $_.DestinationPortRange -notin @("443", "80")
        } | ForEach-Object {
            $report.Add([PSCustomObject]@{
                Check    = "Open NSG Rule"
                Severity = "CRITICAL"
                Resource = $_.Name
                Issue    = "Allows all inbound on port $($_.DestinationPortRange)"
            }) | Out-Null
        }
    }

    # Check 4: SQL Servers without auditing
    Get-AzSqlServer | ForEach-Object {
        $audit = Get-AzSqlServerAudit -ResourceGroupName $_.ResourceGroupName -ServerName $_.ServerName
        if ($audit.BlobStorageTargetState -ne "Enabled") {
            $report.Add([PSCustomObject]@{
                Check    = "SQL Auditing"
                Severity = "MEDIUM"
                Resource = $_.ServerName
                Issue    = "Auditing is not enabled"
            }) | Out-Null
        }
    }

    # Output results
    if ($report.Count -eq 0) {
        Write-Host "All checks passed!" -ForegroundColor Green
    } else {
        Write-Host "Found $($report.Count) issues:" -ForegroundColor Red
        $report | Sort-Object Severity | Format-Table -AutoSize -Wrap
    }
    return $report
}

Essential Az Module Cmdlets Reference

Category Cmdlet Purpose
AuthConnect-AzAccountAuthenticate to Azure
AuthGet-AzContextShow current session info
ResourcesGet-AzResourceList all resources
ResourcesNew-AzResourceGroupCreate resource group
ComputeNew-AzVM / Get-AzVMCreate and manage VMs
ComputeStop-AzVM / Start-AzVMControl VM power state
StorageNew-AzStorageAccountCreate storage account
StorageSet-AzStorageBlobContentUpload files to blob
NetworkingNew-AzVirtualNetworkCreate VNet
SecurityNew-AzNetworkSecurityGroupCreate NSG
Key VaultSet-AzKeyVaultSecretStore secrets securely
RBACNew-AzRoleAssignmentAssign roles to users
DatabaseNew-AzSqlDatabaseCreate SQL database
PolicyNew-AzPolicyAssignmentAssign governance policies
MonitoringAdd-AzMetricAlertRuleV2Create metric alerts
CostsGet-AzConsumptionUsageDetailGet cost/usage data

Azure PowerShell vs Azure CLI

Feature Azure PowerShell (Az) Azure CLI (az)
Output Type.NET objects (strongly typed)JSON strings
Scripting PowerFull programming languageBash scripting (with jq)
Error HandlingTry/Catch/FinallyExit codes + stderr
Cross-PlatformWindows, Linux, macOSWindows, Linux, macOS
Learning CurveModerate (Verb-Noun syntax)Easy (familiar CLI syntax)
IDE IntegrationVS Code + ISE (IntelliSense)Basic tab completion
Azure AutomationNative runbook supportNot natively supported
Best ForComplex automation, enterpriseQuick tasks, Linux admins

Career Impact: PowerShell + Azure Skills

Role Average Salary (US) Average Salary (EU) Job Demand 2026
Azure Administrator (AZ-104)$105,000 – $135,000€65,000 – €90,000Very High
Azure DevOps Engineer$130,000 – $170,000€80,000 – €120,000Very High
Cloud Automation Engineer$120,000 – $160,000€75,000 – €110,000High
PowerShell Developer$95,000 – $130,000€60,000 – €85,000High
Azure Security Engineer (AZ-500)$125,000 – $165,000€75,000 – €115,000Very High

Career Tip: Combining PowerShell + Azure + a certification (AZ-104 or AZ-500) is one of the fastest paths to a six-figure IT salary in 2026. Azure administrators who can automate with PowerShell are significantly more valuable than those who rely solely on the portal.



Further Reading on Dargslan


Conclusion

PowerShell is the automation backbone of Azure. While the Azure Portal is perfect for exploration and one-off tasks, real infrastructure management demands automation. Every VM you create, every network you configure, every security policy you enforce — it should all be codified in PowerShell scripts that are version-controlled, repeatable, and auditable.

The scripts in this guide are production-ready patterns that you can adapt to your environment immediately. Start with authentication and basic VM management, then progress to networking, storage, Key Vault, and cost optimization. Once you're comfortable, build your own Azure Functions with PowerShell triggers for fully automated infrastructure management.

The market rewards Azure PowerShell expertise handsomely. Combining PowerShell automation skills with an AZ-104 or AZ-500 certification is one of the most reliable paths to a $120K+ salary in 2026. The demand for cloud automation engineers continues to outpace supply significantly.

Master Azure PowerShell Automation

From your first cmdlet to production automation scripts:

Get PowerShell for Azure → Get the Lab Workbook →
Share this article:

Stay Updated

Subscribe to our newsletter for the latest tutorials, tips, and exclusive offers.