Most AKS clusters do not break because of a bad release. They break because nobody upgraded them. This tutorial walks through 13 steps that put Azure Kubernetes Service auto-upgrade channels and planned maintenance windows on your cluster, so Kubernetes patches and node images land on a schedule you picked instead of whenever someone remembers. Budget about 60 minutes, and expect most of that to be Azure doing the waiting for you.
The timing is good. Kubernetes 1.37, codenamed Garhwal, has a release post dated August 26, 2026, and Microsoft’s documentation now draws a hard line between AKS Standard and AKS Automatic. On Standard you choose the auto-upgrade channel yourself. On Automatic the channel is fixed to stable, which tracks the latest patch of the previous minor version (called N-1 in the docs). Which model you run decides how much of this tutorial you control.
If you do not have a cluster yet, start with our walkthrough on how to set up Azure AKS from scratch, then come back here. Everything below assumes a working AKS Standard cluster you can safely poke at, ideally a non-production one first.
By the end you will have:
- An auto-upgrade channel chosen on purpose, not left at its default
- A separate node OS upgrade channel, so image updates stop surprising you
- Two planned maintenance windows, one for Kubernetes versions and one for node OS images
- Surge, drain and PodDisruptionBudget settings that let upgrades finish without an outage
- A Terraform project and an audit script you can drop into a repository
What You Need Before You Start
The prerequisites are light, but the permissions matter. Changing an upgrade channel or creating a maintenance configuration needs write access to the cluster resource, so Contributor on the resource group is the practical minimum. You also want a few spare vCPUs of quota in the cluster’s region, because surge upgrades create temporary extra nodes. Running out of quota is the single most common reason a first automated upgrade stalls.
| Requirement | Version or level | Why it matters |
|---|---|---|
| Azure subscription | Contributor on the resource group | Needed to update the cluster and create maintenance configurations |
| Azure CLI | Latest release (run az upgrade) |
Older builds miss newer AKS flags |
| AKS cluster | Standard tier, Kubernetes 1.36 or 1.37 | Starting point for the exercises |
| kubectl | Within one minor version of the cluster (1.36 or 1.37) | Kubernetes supports one minor version of skew either way |
| Terraform | Latest 1.x with the azurerm 4.x provider | Used in the complete project |
| jq and bash | Any current release | Used by the audit script |
| Free quota | A few spare vCPUs in the region | Surge nodes need room |
New to the platform itself? Our Kubernetes cluster tutorial covers the nodes, control plane and kubectl basics this guide takes for granted. You do not need Terraform for the 13 steps. It only shows up in the final project.
One cost note before you build anything. Microsoft’s pricing page lists the AKS Standard tier at $73 per cluster per month and the Premium tier, which adds long-term support, at $438 per cluster per month. Auto-upgrade itself adds no line item, but surge nodes bill as regular virtual machines while they exist.
How AKS Auto-Upgrade Channels Work
An auto-upgrade channel is a policy attached to the cluster. It answers one question: when Azure sees a newer supported Kubernetes version, should it move your cluster, and how far? The channel does not decide the hour of the move. That job belongs to the maintenance window, which comes later in this guide.
| Channel | Behavior | Best for | Main risk |
|---|---|---|---|
none |
Disables automatic upgrades | Clusters managed entirely by hand | Cluster can drift out of support |
patch |
Applies the latest supported patch inside the current minor version | Production clusters that want fixes but no minor jumps | You must schedule minor upgrades yourself |
stable |
Moves to the latest patch of minor version N-1 | Most production clusters | Runs one minor behind the newest release |
rapid |
Tracks the newest supported release | Dev and test clusters | Newest minor gets the least field time |
Microsoft’s documentation also describes a rapid N-1 option, and it recommends pairing whichever channel you pick with a planned maintenance window of at least four hours. The documentation is blunt about none. Leaving a cluster without an effective upgrade channel lets it drift out of support, and that is how most teams end up on a version nobody patches.
Kubernetes upgrades and node OS upgrades are separate
Node OS image upgrades run on their own channel and do not change the Kubernetes version. AKS documents a dedicated NodeImage channel that delivers security and bug-fix images, and reportedly Linux node images refresh weekly while Windows images refresh monthly. Treat the two channels as independent knobs. One controls the Kubernetes minor and patch level. The other controls the operating system underneath it.
Three maintenance configurations, three jobs
AKS uses named maintenance configurations, and the names matter because the CLI will not guess them. The aksManagedAutoUpgradeSchedule configuration schedules Kubernetes version upgrades. The aksManagedNodeOSUpgradeSchedule configuration schedules node OS image upgrades. The default configuration covers weekly AKS component maintenance such as the API server, etcd, CoreDNS and Metrics Server. Mixing them up is the easiest way to build a window that never fires.
Kubernetes 1.37 and the N-1 Rule in November 2026
Here is the part that trips people up this month. According to Microsoft’s AKS supported-versions table, Kubernetes 1.37 reached general availability on AKS in October 2026, after a September preview. The upstream project lists 1.37, 1.36 and 1.35 as its three maintained minor releases. By the documented definition of stable, a cluster on that channel targets the latest patch of the minor version one behind the newest, which is 1.36 while 1.37 is the latest supported minor.
So a stable cluster does not jump to 1.37 today. It waits until 1.38 becomes the latest supported minor, then moves to the newest 1.37 patch. A patch cluster already on 1.37 simply picks up newer 1.37 patches. The newest 1.37 patch confirmed in the sources I checked is 1.37.1, dated September 15, 2026, and a later patch may exist by the time you read this. Always read the live version list in Step 3 rather than trusting any article, this one included.
Kubernetes 1.37 itself shipped 67 enhancements, with 16 graduating to Stable, 23 to Beta and 27 entering Alpha, according to the release coverage. Dynamic Resource Allocation, the framework behind GPU and device scheduling, received another round of changes that the project describes in its Kubernetes v1.37 DRA updates post. None of that changes how channels behave, but it explains why many teams want to land on 1.37 soon. For a deeper look at the version jump itself, see our Kubernetes 1.37 upgrade guide, which covers the breaking changes this tutorial leaves out.
One 1.37 detail is specific to AKS. Microsoft documents that starting with Kubernetes 1.37, an AKS Standard node pool without an explicit LocalDNS profile defaults to Preferred mode, and AKS enables LocalDNS when its compatibility checks pass. If your virtual network uses custom DNS servers, you want to test that before the upgrade, not after. Step 11 covers the check.
Support lifetimes matter just as much as versions. Third-party planners such as this Kubernetes 1.35 end-of-life calendar for EKS, AKS and GKE show how quickly a minor version ages out across providers. An upgrade channel is the cheapest insurance against reading one of those calendars in a panic.
Steps 1-3: Audit the Cluster You Have
Do not change anything yet. The first three steps tell you what the cluster is running, which channels are already set, and which versions Azure offers. Skipping the audit is how people turn on rapid on a production cluster by accident.
Step 1: Sign in and pin your variables. Set the names once so every later command is copy-paste safe.
az login
az account set --subscription "<your-subscription-id>"
export RG="rg-aks-upgrades"
export CLUSTER="aks-upgrades-demo"
export LOCATION="eastus2"
az aks list --resource-group "$RG" --output table
Step 2: Read the current upgrade profile. This shows the Kubernetes version, the pricing tier and both channels in one query.
az aks show --resource-group "$RG" --name "$CLUSTER"
--query "{version:kubernetesVersion, tier:sku.tier, upgradeChannel:autoUpgradeProfile.upgradeChannel, nodeOsChannel:autoUpgradeProfile.nodeOsUpgradeChannel, state:provisioningState}"
--output json
Illustrative output from a cluster that never had its channels configured:
{
"nodeOsChannel": "NodeImage",
"state": "Succeeded",
"tier": "Standard",
"upgradeChannel": null,
"version": "1.36.4"
}
A null upgradeChannel means the cluster only upgrades when a human runs a command. That is the drift scenario Microsoft warns about. Also check state. If it reads anything other than Succeeded, fix that first (see the troubleshooting table), because a failed cluster will not accept a new upgrade policy cleanly.
Step 3: List the versions Azure will actually let you reach. This is your source of truth for what 1.37 means on AKS today.
az aks get-upgrades --resource-group "$RG" --name "$CLUSTER" --output table
az aks get-versions --location "$LOCATION" --output table
The first command lists upgrade targets for your cluster. The second lists every Kubernetes version AKS offers in the region, including preview flags and support plans. If 1.37 does not appear in your region yet, that is a regional rollout gap, not a mistake on your side. Teams that run EKS in parallel will recognize the exercise, and our guide to upgrading an EKS cluster to 1.37 shows the AWS-side equivalent. EKS charges $0.10 per cluster-hour on standard support and $0.60 on extended support, so ignoring version age costs real money there. AKS applies pressure differently, through upgrade channels and a platform enforcement date in 2027 that comes up later.
Steps 4-6: Choose and Set the Auto-Upgrade Channel
Step 4: Pick the channel with a rule, not a hunch. A simple decision rule covers most teams. Use rapid on a sandbox cluster, so you see upgrades first. Use stable on staging and on production clusters that can tolerate a minor-version move every few months. Use patch on production clusters where a minor jump needs a change ticket. Reserve none for clusters you have a written reason to freeze, and put a calendar reminder on that reason.
Microsoft’s October 2026 guidance for individual AKS Standard clusters recommends stable plus planned maintenance. If you buy AKS Long Term Support on the Premium tier, the documentation says to use the patch channel so the cluster keeps receiving the latest supported patches. Those two recommendations pull in different directions, so decide which matters more: staying current, or staying put with security fixes.
Step 5: Create a test cluster with the channel set at creation time. Doing this on a throwaway cluster lets you watch a real upgrade without risking production. Use a small node count, since you only need enough nodes to see surge behavior.
az group create --name "$RG" --location "$LOCATION"
az aks create
--resource-group "$RG"
--name "$CLUSTER"
--tier standard
--node-count 3
--kubernetes-version 1.36
--auto-upgrade-channel stable
--node-os-upgrade-channel NodeImage
--generate-ssh-keys
az aks get-credentials --resource-group "$RG" --name "$CLUSTER" --overwrite-existing
kubectl get nodes
Starting one minor behind gives the stable channel something to work with once Azure’s next supported minor arrives. Starting on 1.36 also matches the upgrade path the Kubernetes project recommends: update to the latest patch of your current minor, then move to the latest patch of the target minor.
Step 6: Change the channel on an existing cluster. This is the command most readers need. It takes effect without restarting any nodes, because it only stores a policy.
az aks update
--resource-group "$RG"
--name "$CLUSTER"
--auto-upgrade-channel stable
az aks show --resource-group "$RG" --name "$CLUSTER"
--query "autoUpgradeProfile" --output json
{
"nodeOsUpgradeChannel": "NodeImage",
"upgradeChannel": "stable"
}
A channel with no maintenance window means upgrades can start whenever Azure decides a new version qualifies. That is fine for a sandbox and a bad idea for anything customers touch. The next three steps close that gap.
Steps 7-9: Add Planned Maintenance Windows
A planned maintenance window tells AKS when an upgrade is allowed to run. It is not a trigger. Microsoft’s documentation describes it as the period in which an already-triggered update may proceed, so a window never forces an upgrade by itself. The channel decides that an upgrade exists, and the window decides when it starts. When you combine the two, the cluster auto-upgrade begins inside the window you configured.
Step 7: Create the Kubernetes version window. Use the exact name aksManagedAutoUpgradeSchedule. The example below runs every Sunday from 01:00 for four hours, which is the minimum length Microsoft recommends.
az aks maintenanceconfiguration add
--resource-group "$RG"
--cluster-name "$CLUSTER"
--name aksManagedAutoUpgradeSchedule
--schedule-type Weekly
--day-of-week Sunday
--interval-weeks 1
--start-time 01:00
--duration 4
--utc-offset +00:00
--start-date 2026-11-08
Pick a time when traffic is lowest and a human can watch if needed. Sunday 01:00 UTC suits a European or US-evening product. A consumer game with a global player base may want a different slot entirely. Two details deserve attention. The --start-date sets when the schedule begins, so a date in the future delays the first eligible window. And --utc-offset is a fixed offset, not a time zone, which matters twice a year (see the pitfalls section).
Step 8: Create the node OS window on a different day. Splitting the two windows is deliberate. If a Kubernetes upgrade and a node image refresh both drain the same pool on the same night, you double the disruption and muddy the cause when something breaks.
az aks maintenanceconfiguration add
--resource-group "$RG"
--cluster-name "$CLUSTER"
--name aksManagedNodeOSUpgradeSchedule
--schedule-type Weekly
--day-of-week Wednesday
--interval-weeks 1
--start-time 01:00
--duration 4
--utc-offset +00:00
--start-date 2026-11-04
Wednesday keeps the image refresh away from the weekend version window and leaves Thursday and Friday free to fix anything that went sideways. If your security team demands faster OS patching, move this window earlier in the week instead of shortening it.
Step 9: Read both windows back and check the cluster’s own view. Never trust a command that printed no error. Confirm the configuration exists under the exact names AKS looks for.
az aks maintenanceconfiguration list
--resource-group "$RG"
--cluster-name "$CLUSTER"
--query "[].{name:name, day:maintenanceWindow.schedule.weekly.dayOfWeek, start:maintenanceWindow.startTime, hours:maintenanceWindow.durationHours}"
--output table
Name Day Start Hours
-------------------------------- --------- ------- -------
aksManagedAutoUpgradeSchedule Sunday 01:00 4
aksManagedNodeOSUpgradeSchedule Wednesday 01:00 4
If you see a configuration called default and nothing else, you have only the platform-component window. That one covers weekly maintenance of AKS components such as CoreDNS and the Metrics Server, and it does not schedule your Kubernetes version upgrades. The output above is illustrative, and your column formatting may differ slightly by CLI version.
Steps 10-11: Make Upgrades Survivable
A schedule only helps if upgrades finish. Two things stop them: too little spare capacity to surge, and workloads that refuse to move. Steps 10 and 11 deal with both, and they are where most real-world failures start.
Step 10: Set max surge and drain timeout per node pool. Surge nodes are extra machines AKS adds during an upgrade so existing pods have somewhere to land. Microsoft’s upgrade guidance reportedly cites a 33% max surge as a production starting point, a default 30-minute node-drain timeout, and at least a four-hour maintenance window. Treat those as starting values and measure.
az aks nodepool list --resource-group "$RG" --cluster-name "$CLUSTER"
--query "[].{pool:name, count:count, surge:upgradeSettings.maxSurge}" --output table
az aks nodepool update
--resource-group "$RG"
--cluster-name "$CLUSTER"
--name nodepool1
--max-surge 33%
--drain-timeout 30
On a three-node pool, 33% rounds to one surge node. That is cheap and slow. On a 30-node pool it is ten extra machines at once, so check your regional vCPU quota against the larger number. If the quota is tight, set an absolute count such as --max-surge 1 and accept longer upgrades. A four-hour window comfortably fits a slow rolling upgrade on a small pool, but a large pool with a long drain timeout can blow through it.
Step 11: Protect workloads with PodDisruptionBudgets and check LocalDNS. A PodDisruptionBudget (PDB) tells Kubernetes how many replicas of an app may be down at once. Too strict a PDB stalls a drain forever. Too loose a PDB lets an upgrade take your service down. A PDB that allows one replica to be unavailable is the usual compromise for apps with three or more replicas.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-pdb
namespace: production
spec:
maxUnavailable: 1
selector:
matchLabels:
app: api
kubectl apply -f api-pdb.yaml
kubectl get pdb --all-namespaces
# Find budgets that can never be satisfied (0 allowed disruptions)
kubectl get pdb --all-namespaces -o json
| jq -r '.items[] | select(.status.disruptionsAllowed == 0) | "(.metadata.namespace)/(.metadata.name)"'
The second command is the one that saves you at 03:00. Any budget with zero allowed disruptions will block a node drain until the timeout expires. Long-lived workloads need extra thought here. Game servers holding live matches are the classic case, and our guide to running a dedicated game server on Kubernetes shows how to keep sessions alive while nodes cycle.
Now the 1.37 check. AKS documentation warns operators to look for an explicit LocalDNS profile before upgrading a node pool to Kubernetes 1.37 or later, and to test custom virtual-network DNS over both UDP and TCP. You can inspect the node pool definition and run a quick DNS probe from inside the cluster.
az aks nodepool show --resource-group "$RG" --cluster-name "$CLUSTER"
--name nodepool1 --output json | grep -i localdns
kubectl run dns-probe --rm -it --restart=Never --image=busybox:stable --
nslookup kubernetes.default.svc.cluster.local
kubectl run dns-probe-ext --rm -it --restart=Never --image=busybox:stable --
nslookup learn.microsoft.com
If the grep returns nothing, the pool has no explicit profile and will pick up the 1.37 default. If your virtual network forwards DNS to a custom resolver, run the external lookup before and after the upgrade, then repeat it against that resolver with TCP forced. A resolver that only answers over UDP is the classic cause of post-upgrade name-resolution failures.
Steps 12-13: Watch the First Upgrade and Verify It
Step 12: Rehearse an upgrade on the test cluster. Do not wait for Sunday to learn how your workloads behave. Trigger one upgrade by hand on the throwaway cluster from Step 5, and watch what the drain does to your PDBs. Open a second terminal to follow the nodes while the upgrade runs.
# Terminal 1: use a full version that the Step 3 output lists
az aks upgrade
--resource-group "$RG"
--name "$CLUSTER"
--kubernetes-version 1.37.1
--yes
# Terminal 2: watch nodes surge, cordon and drain
kubectl get nodes -w
NAME STATUS ROLES AGE VERSION
aks-nodepool1-30481235-vmss000000 Ready <none> 41d v1.36.4
aks-nodepool1-30481235-vmss000001 Ready <none> 41d v1.36.4
aks-nodepool1-30481235-vmss000002 Ready <none> 41d v1.36.4
aks-nodepool1-30481235-vmss000003 Ready <none> 32s v1.37.1
aks-nodepool1-30481235-vmss000000 Ready,SchedulingDisabled <none> 41d v1.36.4
That sample output is illustrative. Your node names, ages and patch version will differ. What to look for is the pattern: a new node appears at the target version, an old node flips to SchedulingDisabled, pods move, and the old node disappears. If the old node sits in SchedulingDisabled for the full drain timeout, a PDB is almost certainly the culprit. Note that a manual upgrade is not subject to your maintenance window, which is exactly why the rehearsal belongs on a test cluster.
Step 13: Verify the final state and set up alerts. After the rehearsal, confirm versions, node health and the activity log. Then script the audit so it runs weekly.
kubectl get nodes -o wide
kubectl get pods --all-namespaces --field-selector=status.phase!=Running
az monitor activity-log list
--resource-group "$RG"
--offset 1d
--query "[?contains(operationName.value, 'managedClusters')].{time:eventTimestamp, op:operationName.localizedValue, status:status.value}"
--output table
Every node should report the same version, and the non-running pod list should be empty or contain only completed jobs. The activity log shows when the cluster operation started and finished, which gives you the real upgrade duration to compare against your four-hour window. For ongoing visibility, route AKS activity into Azure Monitor and alert on failed cluster operations. If you need a quick starting point, an alert on any managed-cluster write operation that ends in a failed status catches both failed upgrades and failed policy changes.
Complete Working Project: Terraform and an Audit Script
The 13 steps are fine for learning. For anything you intend to keep, put the configuration in code so reviewers can see a channel change in a pull request. The project below builds a Standard-tier cluster with the stable channel, a NodeImage OS channel, both maintenance windows and sane surge settings. If you already manage clusters with OpenTofu, our kubeadm and OpenTofu cluster guide shows the same workflow style, and the resource syntax below works unchanged.
terraform {
required_version = ">= 1.5"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
}
variable "location" {
type = string
default = "eastus2"
}
variable "upgrade_channel" {
type = string
default = "stable"
}
resource "azurerm_resource_group" "aks" {
name = "rg-aks-upgrades"
location = var.location
}
resource "azurerm_kubernetes_cluster" "main" {
name = "aks-upgrades-demo"
location = azurerm_resource_group.aks.location
resource_group_name = azurerm_resource_group.aks.name
dns_prefix = "aksupgrades"
sku_tier = "Standard"
automatic_upgrade_channel = var.upgrade_channel
node_os_upgrade_channel = "NodeImage"
default_node_pool {
name = "system"
vm_size = "Standard_D4s_v5"
node_count = 3
upgrade_settings {
max_surge = "33%"
drain_timeout_in_minutes = 30
}
}
identity {
type = "SystemAssigned"
}
maintenance_window_auto_upgrade {
frequency = "Weekly"
interval = 1
duration = 4
day_of_week = "Sunday"
start_time = "01:00"
utc_offset = "+00:00"
}
maintenance_window_node_os {
frequency = "Weekly"
interval = 1
duration = 4
day_of_week = "Wednesday"
start_time = "01:00"
utc_offset = "+00:00"
}
lifecycle {
ignore_changes = [kubernetes_version]
}
}
output "cluster_name" {
value = azurerm_kubernetes_cluster.main.name
}
The ignore_changes line matters. Once a channel upgrades the cluster, the live Kubernetes version no longer matches the one in your state, and without that line Terraform would try to roll it back or flag permanent drift. Apply it with the usual loop.
terraform init
terraform plan -out=aks.tfplan
terraform apply aks.tfplan
Next comes the audit script. It prints every cluster in a subscription with its version, tier, both channels and whether the two named maintenance windows exist. Run it weekly from CI and fail the job when a production cluster has a null channel.
#!/usr/bin/env bash
set -euo pipefail
printf "%-28s %-9s %-10s %-9s %-11s %-6s %-6sn" CLUSTER VERSION TIER CHANNEL NODEOS KUBEWIN OSWIN
az aks list --query "[].{n:name, rg:resourceGroup}" -o json | jq -c '.[]' | while read -r row; do
name=$(echo "$row" | jq -r .n)
rg=$(echo "$row" | jq -r .rg)
info=$(az aks show -g "$rg" -n "$name"
--query "{v:kubernetesVersion, t:sku.tier, c:autoUpgradeProfile.upgradeChannel, o:autoUpgradeProfile.nodeOsUpgradeChannel}" -o json)
wins=$(az aks maintenanceconfiguration list -g "$rg" --cluster-name "$name" --query "[].name" -o tsv)
kwin=no; owin=no
echo "$wins" | grep -q '^aksManagedAutoUpgradeSchedule$' && kwin=yes
echo "$wins" | grep -q '^aksManagedNodeOSUpgradeSchedule$' && owin=yes
printf "%-28s %-9s %-10s %-9s %-11s %-6s %-6sn" "$name"
"$(echo "$info" | jq -r .v)" "$(echo "$info" | jq -r .t)"
"$(echo "$info" | jq -r '.c // "null"')" "$(echo "$info" | jq -r '.o // "null"')" "$kwin" "$owin"
done
CLUSTER VERSION TIER CHANNEL NODEOS KUBEWIN OSWIN
aks-upgrades-demo 1.36.4 Standard stable NodeImage yes yes
aks-legacy-orders 1.34.10 Free null null no no
The second row is the one to fix first. A null channel, no windows and an older minor version is the profile of a cluster that quietly ages out of support. The sample values are illustrative, but the pattern is common in subscriptions that have grown for a few years.
Common Pitfalls That Break AKS Auto-Upgrades
These are the mistakes that show up again and again when teams first turn on automatic upgrades. Each one is cheap to avoid and expensive to debug at night.
- Treating the window as a trigger. A maintenance window only defines when a triggered upgrade may run. If the channel is
none, the window sits idle forever and nobody notices until the version falls out of support. - Making the window too short. Microsoft recommends at least four hours. A 90-minute window on a 20-node pool may start an upgrade it cannot finish, and a half-finished rollout is harder to reason about than a skipped one.
- Trusting a fixed UTC offset across daylight saving changes. The offset in the configuration does not track clock changes. A window set for 01:00 at UTC-05:00 lands an hour off after the next change. Pin the window to
+00:00and convert in your head, or in your runbook. - Assuming stable means newest. The
stablechannel deliberately runs one minor behind. While 1.37 is the latest supported minor, a stable cluster targets the latest 1.36 patch. Teams that want 1.37 now must choosepatchon an already-upgraded cluster orrapid. - Scheduling both windows on the same night. Kubernetes and node OS upgrades both drain nodes. Overlap them and a single bad PodDisruptionBudget fails both, and you lose the clean signal about which change caused the damage. Workloads with long-lived sessions, such as the fleets in our Agones game server scaling guide, make double drains especially painful.
- Letting Terraform fight the channel. If your configuration pins
kubernetes_version, the next apply tries to undo the last automatic upgrade. Useignore_changeson that attribute, as in the project above. - Forgetting surge capacity. A 33% surge on a large pool needs real vCPU quota and real subnet addresses. Check both before the first scheduled run, not after the failure email.
Troubleshooting: 12 Problems and Fixes
When an upgrade misbehaves, start with the cluster state and the activity log, then match the symptom below. Several of these overlap, so work down the list in order.
| # | Symptom | Likely cause | Fix |
|---|---|---|---|
| 1 | No upgrade ever starts | Channel is null or none, or the schedule start date is still in the future |
Run az aks show and check autoUpgradeProfile. Set a channel and verify the start date. |
| 2 | Maintenance configuration is accepted but never fires | Wrong configuration name, so AKS ignores it for upgrades | Use exactly aksManagedAutoUpgradeSchedule or aksManagedNodeOSUpgradeSchedule. |
| 3 | Cluster stays on 1.36 while 1.37 is available | stable targets minor version N-1 by design |
Pick rapid for test clusters, or move production deliberately with a manual upgrade. |
| 4 | Drain hangs, then the upgrade fails | A PodDisruptionBudget allows zero disruptions | List budgets with the jq query in Step 11 and relax the one that blocks. |
| 5 | Surge node creation fails with a quota error | Not enough regional vCPU quota for the extra nodes | Request more quota, or set an absolute --max-surge 1. |
| 6 | Surge fails on address allocation | The subnet has no free IPs for temporary nodes and their pods | Enlarge the subnet or reduce surge. Plan address space for surge nodes up front. |
| 7 | Cluster shows a Failed provisioning state after an upgrade | An interrupted or partially applied operation | Run az aks update -g RG -n CLUSTER with no other flags to reconcile, then retry the upgrade. |
| 8 | Upgrade refuses to proceed and mentions deprecated APIs | Workloads or tooling still call APIs removed in the target version | Check the API server metric apiserver_requested_deprecated_apis, fix the clients, then retry. |
| 9 | Name resolution fails after moving to 1.37 | LocalDNS defaulted to Preferred against a custom resolver that mishandles UDP or TCP |
Test both protocols against the resolver, inspect CoreDNS logs, and set an explicit LocalDNS profile. |
| 10 | Terraform plan shows a Kubernetes version downgrade | The configuration pins kubernetes_version |
Remove the pin or add ignore_changes = [kubernetes_version]. |
| 11 | Upgrades start an hour early or late | Fixed UTC offset across a daylight saving change | Set the window to +00:00 and document the local equivalent. |
| 12 | kubectl prints a version skew warning | Client is more than one minor version from the API server | Install a kubectl release within one minor of the cluster. |
One more item belongs on your radar. Reporting from late October 2026 mentioned that Azure Linux 2.0 node images were scheduled for removal on October 31, 2026. If your pools still run that image family, check your node OS before the next image channel run.
Platform-Initiated Upgrades and the June 2027 Date
Microsoft documented a change this autumn that every AKS owner should understand. Starting June 1, 2027, AKS will begin automatically upgrading clusters that remain on an unsupported Kubernetes version after a 60-day platform-support grace period. That policy is not active today. It is a published future date, and Azure roundups such as Intercept’s October 2026 newsletter flagged it alongside the advice to pair auto-upgrade with planned maintenance.
The details are what make it uncomfortable. According to the documentation, a platform-initiated upgrade applies regardless of the channel you set, including none and patch. It may ignore your planned maintenance window. It does not stop for deprecated API usage. And it may override a PodDisruptionBudget that would otherwise block a node drain. Every protection this tutorial builds can be bypassed by that mechanism.
The practical response is simple. Keep every cluster on a supported minor version, and the platform never needs to act. A stable channel with a four-hour window does that almost for free. The clusters at risk are the ones frozen on none with no owner, which is exactly the profile the audit script flags. Treat a cluster that is two minors behind as an incident-in-waiting, and schedule its upgrade yourself while your windows and PDBs still apply.
AKS Standard vs AKS Automatic: Who Owns the Upgrade Decision
Everything above applies to AKS Standard. AKS Automatic takes most of these decisions away from you, on purpose. Pick the model first, because it determines how much of the configuration you control.
| Upgrade setting | AKS Standard | AKS Automatic |
|---|---|---|
| Kubernetes upgrade channel | Selectable: none, patch, stable or rapid | Fixed to stable, no selection |
| Target version | Depends on the channel you choose | Latest patch of minor version N-1 |
| Node OS channel | Selectable | Fixed NodeImage channel |
| Planned maintenance | Recommended, at least four hours | Supported for scheduling, channel still fixed |
| Long-term support | Premium tier plan, needs the patch channel |
Needs the patch channel, which Automatic does not offer |
| Best fit | Teams with release processes and custom pools | Teams that want Microsoft to carry the upgrade chore |
The trade is control for effort. Automatic suits small teams without a platform engineer, and Standard suits teams that need a specific pool layout or a long support horizon. AWS offers its own hands-off option in EKS Auto Mode, and our EKS vs ECS Auto Mode fee comparison shows how those charges stack up.
If you would rather stop owning Kubernetes upgrades altogether, a different Azure service may fit better. Our guide to setting up Azure Container Apps covers a platform where Microsoft runs the cluster layer for you, at the cost of less Kubernetes surface to tune.
Advanced Tips for Fleets and Production Clusters
Once a single cluster behaves, the next problem is many clusters. These tips come from the patterns that scale past a handful.
- Stage your channels. Run
rapidon a sandbox,stableon staging, andstableorpatchon production, with each stage’s window a few days after the one before it. The gap is your soak time, and it turns one cluster’s failure into an early warning for the rest. - Use Fleet Manager for many clusters. Azure Kubernetes Fleet Manager documentation says update runs honor cluster maintenance windows and can orchestrate updates across AKS clusters, with auto-upgrade profiles to trigger runs. Check the current docs for your tier before building on it.
- Add soak time between nodes. Newer CLI and Terraform builds expose a node soak duration under the upgrade settings. A short soak after each node lets readiness probes and alerts catch a bad rollout before the next node drains.
- Keep a rollback story. AKS upgrades move forward. If a release breaks your app, the realistic recovery is a new node pool on the previous image, traffic shifted by your ingress, and the old pool deleted afterward. Practice it once on the test cluster.
- Alert on version age. Add the audit script to CI and fail the job when any cluster reports a null channel or a minor version older than the supported set.
- Plan Windows and special pools separately. GPU, Windows and spot pools each have their own surge and capacity limits, so give them their own max surge values instead of copying the system pool.
Multi-cloud teams should also standardize the order of operations. The Kubernetes project says to upgrade the API server first, then the controller manager, scheduler and cloud controller, and only then the nodes. Oracle’s managed service follows the same sequence of control plane first, worker-node pools second, and our walkthrough of running Agones game servers on Oracle Cloud OKE shows that pattern in a real workload.
The takeaway: an upgrade policy is a one-time setup that pays off every week afterward. Set the channel, set two windows, test the drain, and keep the audit script running. For more infrastructure walkthroughs, browse our cloud computing tutorials.
FAQ: AKS Auto-Upgrade Channels and Maintenance Windows
Which AKS auto-upgrade channel is best for production?
Microsoft’s current guidance for individual AKS Standard clusters is the stable channel plus a planned maintenance window. Choose patch instead if minor-version jumps need change approval, or if you use AKS Long Term Support, which the documentation pairs with the patch channel.
Does a maintenance window force an upgrade?
No. A window only controls when an already-triggered upgrade may run. The channel creates the upgrade, and the window schedules it. Without a channel, an empty window does nothing.
Will the stable channel move my cluster to Kubernetes 1.37?
Not while 1.37 is the latest supported minor. The stable channel targets the latest patch of minor version N-1, so today it points at 1.36. It will move you to 1.37 after a newer minor becomes the latest supported one.
Can I change the upgrade channel on AKS Automatic?
No. Microsoft documents AKS Automatic as fixed to the stable channel for Kubernetes versions and the NodeImage channel for node OS updates. You can still use planned maintenance to control the timing.
What happens if I leave the channel on none?
The cluster never upgrades on its own and can drift out of support. From June 1, 2027, AKS plans platform-initiated upgrades for clusters still on unsupported versions after a 60-day grace period, and those can ignore your channel and window.
How long should an AKS maintenance window be?
Microsoft recommends at least four hours. Size it from your rehearsal. Take the longest upgrade you measured on the test cluster, scale it by node count, and leave a generous margin.
Do node OS upgrades change the Kubernetes version?
No. Node OS image upgrades run on a separate channel and schedule, and they refresh the operating system image without changing the Kubernetes version. Use aksManagedNodeOSUpgradeSchedule to time them.
Does auto-upgrade cost extra?
The channel itself has no separate charge. You pay the tier price (listed at $73 per cluster per month for Standard and $438 for Premium) plus the cost of any surge nodes while they run.