How to Fix AKS Cluster Auto-Upgrade Not Executing During Scheduled Maintenance Window
Your AKS cluster is configured with Stable auto-upgrade and a scheduled maintenance window — but the upgrade window passed and the cluster version has not changed. No error. No log entry. Just silence. This guide covers all seven reasons an AKS auto-upgrade is silently skipped, with step-by-step diagnostics and fixes for each.
How AKS Auto-Upgrade and Maintenance Windows Actually Work
Before diagnosing why the upgrade did not run, it is important to understand what "scheduled maintenance window" means in AKS — because the mental model most engineers carry is incorrect. A maintenance window in AKS defines a time range during which AKS is permitted to run an upgrade. It does not guarantee that an upgrade will run during every occurrence of that window. Microsoft's documentation states explicitly: maintenance operations are considered best-effort only and are not guaranteed to occur within a specified window.
For an auto-upgrade to actually execute during a maintenance window, all of the following conditions must be true simultaneously:
- The maintenance configuration must be named exactly aksManagedAutoUpgradeSchedule
- The window's startDate must be in the past (the window is active)
- The window duration must be at least 4 hours
- There must be an eligible target Kubernetes version available on the selected upgrade channel
- The cluster must be in a Running (non-stopped) state
- No blocking pre-upgrade validations must be failing (PDB, subnet IPs, quota, API deprecations)
- No Azure regional capacity or resource lock is preventing surge node provisioning
If any one of these conditions is not met, AKS silently skips the upgrade window with no error message and no Activity Log entry. This silent skip behaviour is the primary reason engineers are confused — they see no evidence of an attempted upgrade because no attempt was made.
Immediate Diagnosis — Run These Commands First
Run all of these before diving into the individual causes. The outputs together tell you which gate failed. Save all outputs — you will need them if you open a support ticket.
CLUSTER="clu-r-usw3-tst"
# 1. Current cluster state and version echo "=== CLUSTER STATE ==="
az aks show \
--resource-group "${RG}" --name "${CLUSTER}" \
--query "{Version:kubernetesVersion,UpgradeChannel:autoUpgradeProfile.upgradeChannel,PowerState:powerState.code,ProvisioningState:provisioningState}" \
--output json
# 2. ALL maintenance configurations — check names and startDates echo "=== MAINTENANCE CONFIGURATIONS ==="
az aks maintenanceconfiguration list \
--resource-group "${RG}" --cluster-name "${CLUSTER}" \
--output json
# 3. Available upgrades on the Stable channel echo "=== AVAILABLE UPGRADES ==="
az aks get-upgrades \
--resource-group "${RG}" --name "${CLUSTER}" \
--output table
# 4. Node pool status and versions echo "=== NODE POOLS ==="
az aks nodepool list \
--resource-group "${RG}" --cluster-name "${CLUSTER}" \
--query "[].{Name:name,Version:orchestratorVersion,State:provisioningState,Count:count,MaxSurge:upgradeSettings.maxSurge}" \
--output table
# 5. Resource locks echo "=== RESOURCE LOCKS ==="
az resource lock list \
--resource-group "${RG}" \
--output table
# 6. Activity log around the missed maintenance window (May 30) echo "=== ACTIVITY LOG (May 28–June 1) ==="
RESOURCE_ID=$(az aks show --resource-group "${RG}" --name "${CLUSTER}" --query id -o tsv)
az monitor activity-log list \
--resource-id "${RESOURCE_ID}" \
--start-time 2026-05-28T00:00:00Z \
--end-time 2026-06-01T00:00:00Z \
--query "[].{Time:eventTimestamp,Operation:operationName.value,Status:status.value,Caller:caller}" \
--output table
Cause 1 — startDate Is Set to a Future Date (Most Common)
This is the confirmed root cause for the cluster described in this post. The maintenance configuration output showed "startDate": "2026-06-18" while the expected upgrade window was May 30, 2026. Because the startDate was 19 days in the future at the time of the expected window, AKS had not yet begun honouring the maintenance schedule. The window technically exists but has not become active.
The startDate is the date from which AKS begins evaluating the maintenance schedule. Before this date, no maintenance window occurrences are recognised and no upgrades are triggered. This is a silent condition — there is no warning, no alert, and no Activity Log entry when a window is skipped because the startDate has not been reached.
--resource-group "${RG}" --cluster-name "${CLUSTER}" \
--name aksManagedAutoUpgradeSchedule \
--query "maintenanceWindow.startDate"
# If output is a future date (after today), this is the cause
# Fix: update startDate to today or a recent past date # This activates the maintenance schedule immediately az aks maintenanceconfiguration update \
--resource-group "${RG}" --cluster-name "${CLUSTER}" \
--name aksManagedAutoUpgradeSchedule \
--config-file - <<EOF
{
"properties": {
"maintenanceWindow": {
"schedule": {
"relativeMonthly": {
"dayOfWeek": "Saturday",
"intervalMonths": 1,
"weekIndex": "Last"
}
},
"durationHours": 24,
"startDate": "2026-06-01",
"startTime": "04:00",
"utcOffset": "+00:00"
}
}
}
EOF
# Verify the update az aks maintenanceconfiguration show \
--resource-group "${RG}" --cluster-name "${CLUSTER}" \
--name aksManagedAutoUpgradeSchedule \
--query "maintenanceWindow.startDate"
Cause 2 — Wrong Configuration Name (Not aksManagedAutoUpgradeSchedule)
AKS has three reserved maintenance configuration names, each controlling a different type of update. Only aksManagedAutoUpgradeSchedule gates Kubernetes version auto-upgrades. A configuration named default controls only AKS weekly platform releases — it has no effect on cluster version auto-upgrades. Any other name (including custom names like my-upgrade-schedule or simply maintenance) is completely ignored by the auto-upgrade system.
--resource-group "${RG}" --cluster-name "${CLUSTER}" \
--query "[].name" --output table
# Expected: aksManagedAutoUpgradeSchedule should appear in the list
# If only "default" appears, you need to create the correct configuration
# Fix: create the correct aksManagedAutoUpgradeSchedule configuration # (if the wrong name was used, delete the old one first) az aks maintenanceconfiguration add \
--resource-group "${RG}" --cluster-name "${CLUSTER}" \
--name aksManagedAutoUpgradeSchedule \
--schedule-type RelativeMonthly \
--day-of-week Saturday \
--week-index Last \
--interval-months 1 \
--start-time "04:00" \
--duration 24 \
--utc-offset +00:00 \
--start-date "2026-07-01" # Set to today or recent past
Cause 3 — No Eligible Version Available on the Stable Channel
The most common legitimate reason for a skipped window. AKS's Stable auto-upgrade channel upgrades to the latest patch version of Kubernetes N-1 — the second-most-recent minor version. If your cluster is already on the highest available patch of the Stable channel target, there is nothing to upgrade to and the window is silently skipped.
This can happen if: the Stable channel has not yet published a new version since your last upgrade; your cluster is already on 1.33.x and Stable is targeting 1.33.x as N-1 with no higher patch available; or the new Stable version is still rolling out to West US 3 and has not reached your region yet (AKS releases take up to two weeks to reach all regions via safe deployment practices).
--resource-group "${RG}" --name "${CLUSTER}" \
--output json
# Check all supported Kubernetes versions in West US 3 az aks get-versions --location westus3 --output table
# The Stable channel targets: GA versions that are at least one minor version older # than the latest available. If cluster is on 1.33.7 and Stable targets 1.33.x # with 1.33.7 being the latest patch — no upgrade exists yet.
# Check AKS weekly release tracker for West US 3: # https://releases.aks.azure.com/webpage/index.html
Cause 4 — Maintenance Window Duration Too Short (Less Than 4 Hours)
Microsoft requires a minimum of 4 hours for an aksManagedAutoUpgradeSchedule window to function reliably. A window shorter than 4 hours may not provide enough time for the control plane upgrade and all node pool upgrades to complete. AKS will not start an upgrade operation that cannot complete within the remaining window time — if the window is too short to complete the upgrade, AKS may skip the window entirely rather than start a partial upgrade.
For large clusters with multiple node pools or many nodes, consider 8–12 hours. The cluster in this post has durationHours: 24, so this is not the cause here — but it is a frequent misconfiguration on smaller maintenance windows.
--resource-group "${RG}" --cluster-name "${CLUSTER}" \
--name aksManagedAutoUpgradeSchedule \
--query "maintenanceWindow.durationHours"
# Minimum recommended: 4 hours. For production clusters: 8–12 hours.
# Fix: update duration to at least 4 hours az aks maintenanceconfiguration update \
--resource-group "${RG}" --cluster-name "${CLUSTER}" \
--name aksManagedAutoUpgradeSchedule \
--duration 8
Cause 5 — PDB or Node Pool Blocking Conditions
AKS performs pre-upgrade validations before starting an upgrade. If any validation fails, the upgrade is not initiated. Key validations include checking for Pod Disruption Budgets with maxUnavailable=0 or minAvailable equal to the total replica count (leaving no room for drain), deprecated API usage in workloads, subnet IP exhaustion (insufficient IPs for surge nodes), expired certificates or service principals, and resource locks on the managed cluster resource group.
A PDB that sets maxUnavailable: 0 means no pod in that deployment can be evicted during a node drain — making it impossible for AKS to drain any node. This completely blocks the upgrade without any node being touched. The pre-upgrade validation catches this before the upgrade starts, so it looks exactly like a skipped window from the outside.
# Look for PDBs where ALLOWED DISRUPTIONS = 0 and MIN AVAILABLE = DESIRED (blocking)
# Check for misconfigured PDBs (maxUnavailable=0) kubectl get pdb --all-namespaces -o json | \
jq '.items[] | select(.spec.maxUnavailable==0 or .status.disruptionsAllowed==0) | {name:.metadata.name,namespace:.metadata.namespace,maxUnavailable:.spec.maxUnavailable,disruptionsAllowed:.status.disruptionsAllowed}'
# Check for deprecated API usage (blocks upgrades to versions that remove the API) kubectl get --raw /metrics | grep apiserver_requested_deprecated_apis
# Check subnet available IPs (need IPs for surge nodes) az aks show --resource-group "${RG}" --name "${CLUSTER}" \
--query "agentPoolProfiles[].{Name:name,VnetSubnetID:vnetSubnetId,Count:count,MaxSurge:upgradeSettings.maxSurge}" \
--output table
# Check for resource locks on the managed resource group INFRA_RG=$(az aks show --resource-group "${RG}" --name "${CLUSTER}" --query nodeResourceGroup -o tsv)
az resource lock list --resource-group "${INFRA_RG}" --output table
Cause 6 — Regional Capacity or Quota Constraint
AKS upgrades use max surge to provision temporary extra nodes during the rolling update. If your subscription does not have sufficient vCPU quota in the West US 3 region, or if the region does not have capacity for the node VM size at the time of the upgrade window, the upgrade cannot provision surge nodes and will fail the pre-upgrade validation. Errors that appear in the Activity Log (when an upgrade does attempt) include SKUNotAvailable, AllocationFailed, or OverconstrainedAllocationRequest.
For specialised VM sizes (GPU nodes, high-memory nodes), regional capacity is more constrained than for general-purpose sizes. Check whether your subscription quota in West US 3 is sufficient for the current node count plus the max surge count.
--query "[?contains(name.value, 'cores')].{Name:name.localizedValue,Used:currentValue,Limit:limit}" \
--output table
# Check node pool max surge settings (default is 1 — very conservative) az aks nodepool list \
--resource-group "${RG}" --cluster-name "${CLUSTER}" \
--query "[].{Pool:name,Count:count,SKU:vmSize,MaxSurge:upgradeSettings.maxSurge}" \
--output table
# Recommended: set max surge to 33% for production, or at minimum 1 az aks nodepool update \
--resource-group "${RG}" --cluster-name "${CLUSTER}" \
--name nodepool1 \
--max-surge "33%"
Cause 7 — Cluster Not in Running State During Window
Auto-upgrades only execute when the cluster is in a Running power state. If the cluster was stopped (deallocated) when the maintenance window ran, the upgrade is skipped. Similarly, if the cluster was in a Failed provisioning state from a previous operation, upgrade and scale operations will not be accepted.
--resource-group "${RG}" --name "${CLUSTER}" \
--query "{PowerState:powerState.code,ProvisioningState:provisioningState}" \
--output json
# Expected for upgrades: powerState=Running, provisioningState=Succeeded
# If powerState=Stopped: start the cluster first
# If provisioningState=Failed: resolve the failed state before expecting auto-upgrade
How to Find Auto-Upgrade Activity Logs
A key frustration with AKS auto-upgrade is that when it is silently skipped, there is genuinely nothing to find in the Activity Log — no skipped-upgrade event is recorded. Activity Log entries only exist when an upgrade was attempted. The absence of log entries is itself the diagnostic signal for a silent skip.
When an upgrade does attempt and either succeeds or fails, the Activity Log entries to look for are:
- Create or Update Managed Cluster — triggered when the cluster control plane upgrade starts
- Create or Update Agent Pool — triggered when a node pool upgrade starts
- Upgrade Agent Pool Image Version — for node OS image upgrades specifically
All of these should show Caller as a Microsoft/AKS service identity rather than your user account — this distinguishes auto-upgrades from manually triggered upgrades.
# Search for upgrade-related operations in the last 90 days az monitor activity-log list \
--resource-id "${RESOURCE_ID}" \
--start-time 2026-05-01T00:00:00Z \
--query "[?contains(operationName.value, 'upgrade') || contains(operationName.value, 'Upgrade')].{Time:eventTimestamp,Op:operationName.value,Status:status.value,Caller:caller}" \
--output table
# Search for ALL operations around the maintenance window (May 28–Jun 1) az monitor activity-log list \
--resource-id "${RESOURCE_ID}" \
--start-time 2026-05-28T00:00:00Z \
--end-time 2026-06-01T00:00:00Z \
--query "[].{Time:eventTimestamp,Op:operationName.value,Status:status.value,Caller:caller}" \
--output table
# If nothing appears for upgrade operations during the expected window: # The upgrade was silently skipped — not attempted at all. # Check causes 1-7 above to identify which gate failed.
# For richer upgrade event detail, check Container Insights if enabled: # kubectl get events --field-selector source=upgrader --all-namespaces # (only shows current events unless Container Insights is sending to Log Analytics)
Set Up Monitoring So You Always Know When Upgrades Run
Because a skipped auto-upgrade produces no log entries, you need proactive monitoring to know whether upgrades are running. The best approach is a combination of version comparison alerts and Activity Log alerts for upgrade operations.
| Monitor Signal | What It Tells You | How to Set It Up |
|---|---|---|
| Activity Log alert on Create or Update Managed Cluster | Fires whenever AKS starts a cluster-level operation — including auto-upgrades. Confirms the upgrade was attempted, not just scheduled. | Portal → Cluster → Alerts → Create alert rule → Signal: Activity Log → Create or Update Managed Cluster |
| Kubernetes version metric tracked in Log Analytics | Tracks cluster version over time. A version that has not changed after expected maintenance windows indicates a silent skip pattern. | Enable Container Insights → query KubeNodeInventory | summarize by ClusterVersion on a weekly schedule |
| kubectl events from upgrader | Shows drain, surge, and PDB eviction events during an active upgrade. The key source is source=upgrader. | kubectl get events --field-selector source=upgrader -A during or after a maintenance window |
| Storage of Activity Log to Log Analytics | Extends Activity Log retention beyond 90 days. Enables KQL queries for upgrade history and audit evidence. | Diagnostic settings on the subscription → Send to Log Analytics Workspace → Category: Administrative |
--resource-group "${RG}" \
--name "aks-upgrade-alerts" \
--short-name "aksupgrade" \
--action email platform-team "platform@yourcompany.com"
AG_ID=$(az monitor action-group show --resource-group "${RG}" --name "aks-upgrade-alerts" --query id -o tsv)
RESOURCE_ID=$(az aks show --resource-group "${RG}" --name "${CLUSTER}" --query id -o tsv)
# Alert when an upgrade operation starts on this cluster az monitor activity-log alert create \
--name "aks-upgrade-started" \
--resource-group "${RG}" \
--scope "${RESOURCE_ID}" \
--condition "category=Administrative and operationName=Microsoft.ContainerService/managedClusters/write" \
--action-group "${AG_ID}" \
--description "AKS cluster update/upgrade operation started — verify it is the expected auto-upgrade"
Key Takeaways
Related FAVRITE Articles
- How to Fix Azure Database for PostgreSQL Flexible Server Stuck in Starting State
- Least Privileged Access Required to View Azure App Service Log Stream
- Top 50 Azure Cloud Administrator Interview Questions and Answers