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 Login | Connect-AzAccount (browser popup) | High | Manual administration, learning |
| Service Principal | App registration + client secret/cert | High | CI/CD pipelines, automation scripts |
| Managed Identity | Azure-assigned identity (no credentials) | Highest | Azure VMs, Functions, App Service |
| Device Code | Code-based auth for headless systems | Medium | SSH sessions, remote servers |
| Access Token | Pre-obtained OAuth token | Medium | Short-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 |
|---|---|---|
| Auth | Connect-AzAccount | Authenticate to Azure |
| Auth | Get-AzContext | Show current session info |
| Resources | Get-AzResource | List all resources |
| Resources | New-AzResourceGroup | Create resource group |
| Compute | New-AzVM / Get-AzVM | Create and manage VMs |
| Compute | Stop-AzVM / Start-AzVM | Control VM power state |
| Storage | New-AzStorageAccount | Create storage account |
| Storage | Set-AzStorageBlobContent | Upload files to blob |
| Networking | New-AzVirtualNetwork | Create VNet |
| Security | New-AzNetworkSecurityGroup | Create NSG |
| Key Vault | Set-AzKeyVaultSecret | Store secrets securely |
| RBAC | New-AzRoleAssignment | Assign roles to users |
| Database | New-AzSqlDatabase | Create SQL database |
| Policy | New-AzPolicyAssignment | Assign governance policies |
| Monitoring | Add-AzMetricAlertRuleV2 | Create metric alerts |
| Costs | Get-AzConsumptionUsageDetail | Get cost/usage data |
Azure PowerShell vs Azure CLI
| Feature | Azure PowerShell (Az) | Azure CLI (az) |
|---|---|---|
| Output Type | .NET objects (strongly typed) | JSON strings |
| Scripting Power | Full programming language | Bash scripting (with jq) |
| Error Handling | Try/Catch/Finally | Exit codes + stderr |
| Cross-Platform | Windows, Linux, macOS | Windows, Linux, macOS |
| Learning Curve | Moderate (Verb-Noun syntax) | Easy (familiar CLI syntax) |
| IDE Integration | VS Code + ISE (IntelliSense) | Basic tab completion |
| Azure Automation | Native runbook support | Not natively supported |
| Best For | Complex automation, enterprise | Quick 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,000 | Very High |
| Azure DevOps Engineer | $130,000 – $170,000 | €80,000 – €120,000 | Very High |
| Cloud Automation Engineer | $120,000 – $160,000 | €75,000 – €110,000 | High |
| PowerShell Developer | $95,000 – $130,000 | €60,000 – €85,000 | High |
| Azure Security Engineer (AZ-500) | $125,000 – $165,000 | €75,000 – €115,000 | Very 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.
Recommended Books for Azure PowerShell & Automation:
- PowerShell for Azure Administration — €14.90
- Hands-On Azure PowerShell Lab Workbook — €8.90
- Azure for System Administrators — €27.90
- PowerShell 7 Fundamentals — €24.90
- Mastering PowerShell — €14.90
- Azure Active Directory (Microsoft Entra ID) — €24.90
- PowerShell for Active Directory — €22.90
- Bash vs PowerShell: Cross-Platform Scripting — €21.90
- Windows Server 2025: Manage with PowerShell — €14.90
- Exchange Online Management with PowerShell — €11.90
- Automating Microsoft 365 with Python — €12.90
- Ansible Automation: From Zero to Production — €24.90
- Webhook Automation in Practice — €12.90
Further Reading on Dargslan
- Microsoft Azure Complete Guide 2026
- AWS vs Azure vs GCP 2026: Cloud Comparison
- Linux Security Hardening 2026
- Wazuh SIEM Complete Guide 2026
- Docker vs Kubernetes: Comparison Guide
- IT Certification Roadmap 2026
- SOC Analyst Career Guide 2026
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 →