Is Your Azure Service Retiring? Build a PowerShell Monitor

Published:9 February 2026 - 7 min. read

Audit your Active Directory for weak passwords and risky accounts. Run your free Specops scan now!

You deployed a service to Azure three months ago. It works. Traffic flows. Nobody complains. Then you get an email: “Your Azure service will be retired in 90 days. Migrate immediately.” And you have no idea which service they’re talking about because your environment has 400 resources spread across six subscriptions.

Sound familiar? Azure operates on a continuous lifecycle model where services get upgraded or deprecated without warning your entire team. The Service Retirement workbook provides a centralized view, but checking a portal dashboard every week isn’t automation—it’s just a faster way to miss deadlines.

Here’s how to build a PowerShell monitor that queries Azure Resource Graph (ARG), identifies retiring services, and alerts your team before those 90 days become 9.

Prerequisites

You’ll need:

  • Azure PowerShell module (Az.ResourceGraph)

  • Reader access to the subscriptions you want to monitor

  • A service principal or managed identity (Azure’s way of giving automation accounts their own credentials) if running in Azure Automation

Verify your environment:

Get-Module -ListAvailable Az.ResourceGraph

If it’s not installed:

Install-Module -Name Az.ResourceGraph -Scope CurrentUser

Authenticate to Azure:

Connect-AzAccount

Pro Tip: If you’re planning to run this as an automated runbook, use a system-assigned managed identity instead of interactive authentication. Grant the identity Reader access at the management group level to query across all subscriptions.


Understanding Azure’s Retirement Data Model

Azure doesn’t have a single “retirement API.” Retirement data is scattered across two distinct systems, and you need to query both to get the full picture.

Data Source What It Tells You Table in ARG
Azure Service Health High-level retirement announcements (e.g., “API version X is deprecated”) ServiceHealthResources
Azure Advisor Specific resources impacted by retirements AdvisorResources

Service Health tracks the event itself—the announcement that something is retiring. Azure Advisor scans your deployed resources and flags which ones are actually affected.

Both are queryable via Azure Resource Graph, Microsoft’s cross-subscription query engine that uses Kusto Query Language (KQL). Think of ARG as SQL for your entire Azure environment—subscriptions, resource groups, compliance data, and service health events.

Query Service Health for Retirement Announcements

Start by finding all active retirement events across your tenant. The ServiceHealthResources table contains health advisories, and retirements are a specific event subtype.

$query = @"
ServiceHealthResources
| where type =~ 'Microsoft.ResourceHealth/events'
| extend eventType = properties.EventType
| extend eventSubType = properties.EventSubType
| where eventType == 'HealthAdvisory' and eventSubType == 'Retirement'
| project TrackingId=properties.TrackingId,
          Title=properties.Title,
          ImpactStartTime=properties.ImpactStartTime
"@

$retirementEvents = Search-AzGraph -Query $query
$retirementEvents | Format-Table

This returns every retirement announcement Azure has issued that’s still within the 60-day retention window. Yes, 60 days—not 90. Retirement notifications in Service Health are retained for 60 days, so if you want historical tracking, you’ll need to export this data to Log Analytics or a storage account.

The output includes:

  • TrackingId: Unique identifier for the event

  • Title: Human-readable description of the retirement

  • ImpactStartTime: When the retirement was announced

This tells you what is retiring. It doesn’t tell you which of your resources are impacted.

Query Advisor for Impacted Resources

To find out which specific VMs, storage accounts, or app services need migration, query the AdvisorResources table. Advisor scans your environment and generates recommendations for resources that match retirement signatures.

$impactQuery = @"
advisorresources
| where type == 'microsoft.advisor/recommendations'
| where properties.category == 'Reliability'
| where properties.extendedProperties.recommendationSubCategory == 'ServiceUpgradeAndRetirement'
| extend retirementFeatureName = properties.extendedProperties.retirementFeatureName
| extend retirementDate = properties.extendedProperties.retirementDate
| extend resourceId = properties.resourceMetadata.resourceId
| project retirementFeatureName, retirementDate, resourceId
"@

$impactedResources = Search-AzGraph -Query $impactQuery
$impactedResources | Format-Table

This returns every resource ID flagged for retirement, along with the feature name and deadline. The query uses extendedProperties fields that contain retirement-specific metadata—these properties may not be populated consistently across all retirement scenarios, but they provide the most detailed information when available.


Reality Check: The Advisor data isn’t exhaustive. It covers approximately 40 services and features—primarily infrastructure retirements like VM series or API versions. If you’re using a niche preview service, you might not get advance warning here.


Handling Pagination for Large Tenants

If your environment has hundreds of flagged resources, Search-AzGraph will truncate results at 1,000 records. You need to implement pagination using the -SkipToken parameter.

Here’s how to retrieve all results:

$allResults = @()
$skipToken = $null

do {
    if ($skipToken) {
        $response = Search-AzGraph -Query $impactQuery -First 1000 -SkipToken $skipToken
    } else {
        $response = Search-AzGraph -Query $impactQuery -First 1000
    }

    $allResults += $response
    $skipToken = $response.SkipToken
} while ($skipToken)

$allResults | Format-Table

This loop continues querying until ARG stops returning a skip token, meaning you’ve retrieved the full dataset. Skip this step if you’re confident your tenant has fewer than 1,000 impacted resources. (You probably don’t know that number off the top of your head, which is exactly why you’re building this monitor.)

Enriching Results with the Azure EOL Repository

Microsoft maintains an open-source EOL repository on GitHub containing a JSON list of all Azure retirements. This file includes migration links and human-readable descriptions that aren’t always present in the ARG query results. The repository serves as a single source of truth for retirement documentation—something that becomes critical when you’re trying to explain to management why that production VM needs to move by next quarter.

Download the list:

$eolUrl = "https://raw.githubusercontent.com/Azure/EOL/main/service_list.json"
$eolData = Invoke-RestMethod -Uri $eolUrl

Now you can cross-reference the ARG results with the EOL data to add migration guidance:

$enrichedResults = $allResults | ForEach-Object {
    $feature = $_.retirementFeatureName
    $eolEntry = $eolData | Where-Object { $_.RetiringFeature -eq $feature }

    [PSCustomObject]@{
        ResourceId = $_.resourceId
        RetirementFeature = $feature
        RetirementDate = $_.retirementDate
        MigrationLink = $eolEntry.Link
    }
}

$enrichedResults | Format-Table

This gives you actionable data: which resources are affected, when they retire, and where to find the migration guide. The migration link is what transforms this from “we have a problem” into “here’s how to fix it”—the difference between panic and a project plan.

Building the Monitor Script

Combine everything into a single monitor script that runs on a schedule. Save this as Monitor-AzureRetirements.ps1—and yes, the filename matters when your future self tries to find it six months from now.

The script performs three core operations:

Operation Purpose Output
Query Service Health Retrieve active retirement advisories Retirement event metadata (tracking IDs, service names)
Query Advisor Identify impacted resources Resource IDs flagged for retirement
Export Results Persist data for tracking CSV file with resource details
#Requires -Modules Az.ResourceGraph

Connect-AzAccount

# Query for retirement advisories
$healthQuery = @"
ServiceHealthResources
| where type =~ 'Microsoft.ResourceHealth/events'
| extend eventType = properties.EventType
| extend eventSubType = properties.EventSubType
| where eventType == 'HealthAdvisory' and eventSubType == 'Retirement'
| project TrackingId=properties.TrackingId, Title=properties.Title
"@

$retirementEvents = Search-AzGraph -Query $healthQuery

# Query for impacted resources with pagination
$impactQuery = @"
advisorresources
| where type == 'microsoft.advisor/recommendations'
| where properties.category == 'Reliability'
| where properties.extendedProperties.recommendationSubCategory == 'ServiceUpgradeAndRetirement'
| extend retirementFeatureName = properties.extendedProperties.retirementFeatureName
| extend retirementDate = properties.extendedProperties.retirementDate
| extend resourceId = properties.resourceMetadata.resourceId
| project retirementFeatureName, retirementDate, resourceId
"@

$allResources = @()
$skipToken = $null

do {
    if ($skipToken) {
        $response = Search-AzGraph -Query $impactQuery -First 1000 -SkipToken $skipToken
    } else {
        $response = Search-AzGraph -Query $impactQuery -First 1000
    }
    $allResources += $response
    $skipToken = $response.SkipToken
} while ($skipToken)

# Output results
Write-Host "Found $($retirementEvents.Count) retirement events"
Write-Host "Found $($allResources.Count) impacted resources"

$allResources | Export-Csv -Path "AzureRetirements.csv" -NoTypeInformation

Run it:

.\Monitor-AzureRetirements.ps1

Check the output file:

Import-Csv AzureRetirements.csv | Format-Table

Automating with Azure Automation

Running this manually defeats the purpose. Deploy it as an Azure Automation runbook that executes daily. If you’re still logging into the portal to run scripts in 2026, you’re not automating—you’re just clicking with more steps.

Create a runbook:

$automationAccount = "MyAutomationAccount"
$resourceGroup = "MyResourceGroup"

New-AzAutomationRunbook -Name "Monitor-Retirements" `
    -Type PowerShell `
    -AutomationAccountName $automationAccount `
    -ResourceGroupName $resourceGroup

Upload the script:

Import-AzAutomationRunbook -Path ".\Monitor-AzureRetirements.ps1" `
    -Name "Monitor-Retirements" `
    -Type PowerShell `
    -AutomationAccountName $automationAccount `
    -ResourceGroupName $resourceGroup `
    -Force

Publish it:

Publish-AzAutomationRunbook -Name "Monitor-Retirements" `
    -AutomationAccountName $automationAccount `
    -ResourceGroupName $resourceGroup

Schedule it to run daily:

$schedule = New-AzAutomationSchedule -Name "DailyRetirementCheck" `
    -AutomationAccountName $automationAccount `
    -ResourceGroupName $resourceGroup `
    -StartTime (Get-Date).AddHours(1) `
    -DayInterval 1

Register-AzAutomationScheduledRunbook -RunbookName "Monitor-Retirements" `
    -ScheduleName "DailyRetirementCheck" `
    -AutomationAccountName $automationAccount `
    -ResourceGroupName $resourceGroup

The runbook will use the automation account’s system-assigned managed identity to authenticate. Grant the identity Reader access to your management group or subscriptions—and document which identity has which permissions, because the next person inheriting this automation will thank you.

Approach Execution Notification Scalability
Manual Script Interactive login required None Doesn’t scale beyond a single operator
Scheduled Runbook Runs automatically daily Requires integration (Teams, email) Scales across entire tenant

Warning: If your runbook fails with an authentication error, verify the managed identity has Reader permissions. The error message won’t tell you that—it’ll just say “unauthorized.”


Alerting via Microsoft Teams

Generating a CSV file in an automation account doesn’t notify anyone. Pipe the results to a Microsoft Teams webhook so your engineering team actually sees the alerts. A retirement notification sitting in a storage account is just a different kind of silence—louder than nothing, but not loud enough.

Create an Incoming Webhook in your Teams channel, then add this to the runbook:

$teamsWebhook = "https://outlook.office.com/webhook/YOUR-WEBHOOK-URL"

if ($allResources.Count -gt 0) {
    $message = @{
        text = "Azure Retirement Alert: $($allResources.Count) resources require migration"
    } | ConvertTo-Json

    Invoke-RestMethod -Method Post -Uri $teamsWebhook -Body $message -ContentType "application/json"
}

Now every time the script finds impacted resources, it posts a notification to Teams. Your team sees the alert before the service disappears. The real value isn’t the webhook—it’s the 90-day head start it buys you.

From Reactive Emails to Proactive Governance

You built a PowerShell monitor that queries Azure Resource Graph for retirement advisories, identifies specific impacted resources, handles pagination for large datasets, and sends alerts to Microsoft Teams—turning a reactive email-based process into proactive governance.

Here’s what the complete solution delivers:

Component Data Source Output Business Value
Retirement Scanner Service Health events via ARG Active retirement announcements Know what’s retiring across Azure
Resource Identifier Advisor recommendations via ARG Specific resource IDs flagged for migration Know which of your resources are impacted
Migration Enrichment Azure EOL GitHub repository Links to official migration guides Know how to fix the problem
Automation Layer Azure Automation scheduled runbook Runs daily without manual intervention Continuous monitoring at scale
Alert Distribution Microsoft Teams webhook Real-time notifications to engineering team Act on retirements before deadlines

Azure’s continuous lifecycle model means retirements are inevitable. The difference between a forced migration at 2 AM and a planned weekend maintenance window is whether you knew 90 days in advance or 9. This monitor ensures you always know first.

Hate ads? Want to support the writer? Get many of our tutorials packaged as an ATA Guidebook.

Explore ATA Guidebooks

Looks like you're offline!