Kubernetes cgroup v2 migration go or no-go

Audit the fleet, prove workload compatibility and cost a reversible node-pool replacement before approving your upgrade.

23 October 2026 · 9 min read · Cloudline Consulting

Three engineers reviewing a deployment plan in an office beside a server room.
Photo: Dunedin City Council Archives (BY 2.0)

Your Kubernetes upgrade is booked, but nobody can prove what the custom AMI actually boots with. That is not a minor documentation gap. From Kubernetes v1.35, the upstream kubelet refuses to start on cgroup v1 by default. A successful migration needs more than a newer image: it needs evidence that applications, agents, drains and rollback work together. Use this six-gate runbook to turn an uncertain upgrade into a defensible go/no-go decision, with commands, acceptance criteria and a temporary-capacity budget.

In this article
  1. Gate the node upgrade against the rules that actually apply
  2. Find every legacy node and every gap in the evidence
  3. Prove applications and memory monitoring still tell the truth
  4. Build an EKS AL2023 image without changing everything at once
  5. Cost the canary and rehearse both directions
  6. Make the release decision and give every exception an expiry

Gate the node upgrade against the rules that actually apply

Two engineers reviewing a deployment diagram on a whiteboard.
Approve the node stack, not just the Kubernetes version. Photo: classroomcamera (BY 2.0)

Use Cloudline’s six evidence gates as your Kubernetes upgrade checklist. Each needs an artefact and a named individual accountable for it, not just a team. An unknown blocks approval.

  • Policy: applicability record; platform lead owns it.
  • Inventory: node and runtime register; infrastructure lead owns it.
  • Compatibility: application and cgroup v2 memory monitoring results; application lead owns it.
  • Image: reproducible image manifest; image maintainer owns it.
  • Rehearsal: canary and rollback evidence; SRE lead owns it.
  • Release: signed decision and exception register; release lead owns it.

For a Kubernetes cgroup v2 migration, distinguish upstream defaults from provider behaviour. A control-plane version alone does not establish node compatibility.

ScopeRule that gates approval
Upstream v1.35failCgroupV1 defaults to true. On cgroup v1, the affected kubelet fails to start when it starts—not simply when the control plane changes version.
kubeadm v1.35A cgroup-v1 SystemVerification preflight check also fails. Changing the kubelet setting alone does not resolve it.
EKS 1.35EKS release notes document AL2023 using v2, Bottlerocket using v2 with failCgroupV1: false, and Fargate continuing with v1. This is also EKS’s last release supporting containerd 1.x.

Temporary exception, not a migration: this KubeletConfiguration fragment permits cgroup v1 where applicable.

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
failCgroupV1: false

The 6 October 2026 blog describes removal as scheduled for v1.38. However, KEP-5573 specifies no fixed removal timeline: no earlier than v1.38, not a guaranteed deadline.

Do not declare all managed compute unsupported from an upstream default. EKS AL2023 migration needs joined-up Kubernetes, OS and runtime planning—the scope of Cloudline’s cloud, DevOps and Kubernetes services.

A version number is not an upgrade-readiness assessment.

Share this quote

Find every legacy node and every gap in the evidence

An engineer working at a laptop beside a notebook.
Unknown nodes belong on the blocker list. Photo: wocintechchat.com (BY 2.0)

Inventory every Linux node across explicitly selected contexts. For a Kubernetes cgroup v2 migration, an image name is not evidence. Custom AMIs and replacement nodes can differ from the pool they supposedly belong to.

Save this as audit.sh, then run bash audit.sh production-uk staging-uk > nodes.tsv. It requires kubectl and jq, never changes your active context, and creates no Pods.

#!/usr/bin/env bash
set -euo pipefail
(( $# )) || { echo 'Usage: audit.sh CONTEXT [CONTEXT...]' >&2; exit 2; }
for cmd in kubectl jq; do
  command -v "$cmd" >/dev/null || {
    echo "Missing dependency: $cmd" >&2; exit 2;
  }
done
printf 'context\tnode\tpool\tos\tkernel\truntime\tkubelet\tcgroup\n'
for ctx in "$@"; do
  nodes=$(kubectl --context="$ctx" --request-timeout=20s get nodes -o json)
  while IFS= read -r node; do
    name=$(jq -r '.metadata.name' <<<"$node")
    metrics=''
    if ! metrics=$(kubectl --context="$ctx" --request-timeout=10s \
      get --raw "/api/v1/nodes/$name/proxy/metrics"); then
      printf 'WARNING: %s/%s metrics unavailable\n' "$ctx" "$name" >&2
      metrics=''
    fi
    cg=$(awk '/^kubelet_cgroup_version([{ ]|$)/ {print $NF; exit}' <<<"$metrics")
    case "$cg" in
      1|2) ;;
      *) cg=UNKNOWN
         printf 'WARNING: %s/%s cgroup UNKNOWN\n' "$ctx" "$name" >&2 ;;
    esac
    jq -r --arg ctx "$ctx" --arg cg "$cg" '[
      $ctx, .metadata.name,
      (.metadata.labels["eks.amazonaws.com/nodegroup"] //
       .metadata.labels["karpenter.sh/nodepool"] // "UNLABELLED"),
      .status.nodeInfo.osImage, .status.nodeInfo.kernelVersion,
      .status.nodeInfo.containerRuntimeVersion,
      .status.nodeInfo.kubeletVersion, $cg
    ] | @tsv' <<<"$node"
  done < <(jq -c '.items[] | select(.status.nodeInfo.operatingSystem == "linux")' <<<"$nodes")
done

Use an approved audit identity. Access to nodes/proxy is sensitive: it exposes kubelet endpoints, not merely harmless inventory. Do not grant it casually to make this script pass.

KEP-5573 identifies kubelet_cgroup_version for migration monitoring. Values 1 and 2 provide runtime evidence; denied access, unreachable kubelets and missing metrics remain UNKNOWN. Unknown does not mean compatible.

Reconcile what Kubernetes cannot see

  • Match registered nodes to EC2 instances, including stopped instances and machines that failed to join.
  • Compare managed node-group and Auto Scaling group desired capacity with registered capacity. Investigate every mismatch.
  • Inspect scale-from-zero node groups and Karpenter NodeClasses. Record their resolved AMIs and launch-template versions, not just today’s running images.

A node where the kubelet fails to start may never enter this report. Check bootstrap logs and effective failCgroupV1 settings rather than treating an empty pool as clean.

For node-level confirmation, use an approved host session, such as Systems Manager Session Manager:

stat -fc %T /sys/fs/cgroup/
findmnt -R /sys/fs/cgroup
cat /proc/1/cgroup

Run these against the host, not an arbitrary application container. cgroup2fs confirms the unified mount; tmpfs needs investigation for legacy or hybrid layouts.

Collect effective kubelet configuration, the runtime’s cgroup driver, AMI ID and launch-template version separately. OS labels alone prove neither configuration nor compatibility. Cloudline’s Understanding Cloud Risks explains the operational cost of undocumented infrastructure.

Prove applications and memory monitoring still tell the truth

Two desktop monitors displaying charts too small to read.
A green dashboard is useful only when its measurements are trustworthy. Photo: milele (BY 2.0)

Make application compatibility a release gate for your Kubernetes cgroup v2 migration. Record deployed versions against this matrix; version checks alone cannot earn a pass.

ComponentDeployed versionEvidenceOwnerPass/fail
JVM applicationsjava -versionDetected memory matches limitApplication leadFail: host-sized heap
Node.js applicationsnode --versionRuntime recognises container limitApplication leadFail: host memory detected
Go automaxprocs usersEmbedded module versionCPU quota and GOMAXPROCS agreeService ownerFail: incorrect parallelism
Standalone cAdvisorImage tag and digestMetrics survive migrationObservability leadFail: missing series
Security agentsAgent buildSupported configuration; detection testSecurity leadFail: lost detection
Custom scriptsRepository commitExercise every /sys/fs/cgroup readerPlatform leadFail: v1-only paths

Kubernetes lists compatibility floors including OpenJDK 8u372 and 11.0.16, standalone cAdvisor 0.43.0 and automaxprocs 1.5.1. These are not recommended lifecycle targets. Its guidance also warns that unsupported Node.js versions can detect host memory instead of container limits.

AWS documents older JDK8 builds sizing heaps from node memory on AL2023, causing OOM exceptions and Pod restarts. Run inside a representative container; compare detected memory with its configured limit.

kubectl exec memory-probe -c app -- java -XshowSettings:system -version
kubectl get pod memory-probe -o jsonpath='{.spec.containers[?(@.name=="app")].resources}'

For cgroup v2 memory monitoring, retrieve that container’s runtime ID from the API:

kubectl get pod memory-probe -o jsonpath='{.status.containerStatuses[?(@.name=="app")].containerID}'

On its node, set CID to that ID without containerd://. Follow the upstream PID-to-path method:

PID=$(sudo crictl inspect "$CID" | jq -r '.info.pid')
CG=$(sudo awk -F: '$1=="0"{print $3}' /proc/$PID/cgroup)
sudo grep . "/sys/fs/cgroup$CG"/memory.{current,max,stat,events}

Compare old/new nodes under equal load: cgroup counters, Prometheus working-set/RSS series and CloudWatch ingestion. Working set includes kernel and recently accessed memory, not just application heap. Induce a memory-limit breach in an isolated workload; verify OOM counters, restarts, missing-series alerts and actual alert delivery.

Do not silence changed graphs by raising limits: establish whether accounting or application behaviour changed first. Use Cloudline’s Kubernetes Pod sizing guide for requests, limits and measured headroom.

Build an EKS AL2023 image without changing everything at once

Rows of servers in a data-centre rack.
Qualify the replacement image before expanding the change. Photo: mikeshelby (BY 2.0)

Keep the EKS AL2023 migration separate from the Kubernetes version change. Record actual current and target minors in the Kubernetes upgrade checklist; “upgrade EKS” is not a sequence.

  1. For a planned 1.35 → 1.36 upgrade, qualify AL2023 with 1.35 nodes under the unchanged 1.35 control plane, where supported.
  2. Complete the OS replacement and drain the old pools.
  3. Upgrade the control plane to 1.36, then the nodes. Never run a kubelet newer than the control plane.

Check EKS prerequisites and available AL2023 AMIs for those exact minors. An April 2024 migration example is not a current compatibility matrix.

AL2023 uses nodeadm and NodeConfig, not copied AL2 bootstrap.sh arguments. Within the launch template’s MIME user data, include an application/node.eks.aws part. Validate field support against the selected kubelet before using this fragment:

apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
  kubelet:
    config:
      cgroupDriver: systemd
      failCgroupV1: true

Custom-AMI and self-managed paths also need cluster name, API endpoint, certificate authority and service CIDR under spec.cluster. With supported failCgroupV1 enforcement, the kubelet fails to start on cgroup v1. AL2023 cgroup v1 is not a supported migration destination.

The documented VPC CNI minimum is 1.16.2; select an add-on release compatible with both migration-stage and target minors. Test IMDSv2-dependent credentials, Pod Identity or IRSA, DNS, EBS mounts and log shipping.

Standard managed node groups without a specified custom AMI require blue/green replacement. Custom-AMI launch templates have different update mechanics; prefer a separate canary group anyway.

The diagram shows an unchanged control plane above old and canary pools, shared networking and storage, and identical observability paths. Cloudline’s platform engineering services cover reproducible Terraform and image pipelines.

Cost the canary and rehearse both directions

Three engineers planning a staged deployment around a whiteboard.
Buy enough overlap to test recovery, not just deployment. Photo: locusresearch (BY 2.0)

Budget overlap, not just replacement. For this Kubernetes cgroup v2 migration planning example—not an AWS quote—a 12-node fleet adds three equivalent canary nodes, one per Availability Zone.

48-hour overlapPeak nodesExtra capacityIncremental compute
Three canaries1525%3 × 48 × £0.20 = £28.80
Full duplicate pool24100%12 × 48 × £0.20 = £115.20

The assumed effective rate is £0.20 per node-hour. Generalise with Σ(extra nodes × hours × effective hourly rate). Add EBS, monitoring ingestion, network traffic and applicable tax separately. See Understanding Cloud Cost Dynamics for effective rates and temporary-capacity trade-offs.

Three nodes are not a universal minimum. Check allocatable CPU, memory and Pod slots after DaemonSet overhead, plus subnet IPs and AWS quotas.

  1. Retain the old pool. Create canaries labelled migration=cgroup2 and tainted migration=cgroup2:NoSchedule.
  2. Select representative workloads explicitly using node selectors and tolerations. Ensure every required DaemonSet tolerates the taint.
  3. Run load, batch, storage and restart tests. Then migrate one old node at a time.
kubectl get pdb -A
kubectl drain "$OLD_NODE" --ignore-daemonsets --timeout=15m

Drain uses eviction and respects PDBs; --disable-eviction bypasses that protection. An emptyDir warning requires a deliberate data-loss decision, not an automatic extra flag.

Rehearse with three service replicas, a PDB specifying minAvailable: 2, and topology spread across zones using maxSkew: 1. Deliberately make one replica unready, then drain a node hosting another: eviction should block. Restore readiness, correct replica placement or revise the budget—not force-delete Pods. PDBs do not prevent involuntary failures.

Example team-set thresholds: one complete peak/batch cycle; all required agents healthy; an alert delivered within five minutes; no unexplained OOMs; p95 latency within the agreed SLO.

Rehearse rollback: restore workload placement to retained old nodes, drain the canaries and confirm recovery. Retain sufficient old capacity; constrain autoscaler and Karpenter changes so they cannot remove your escape route.

The rollback is not real until workloads have made the return journey.

Share this quote

Make the release decision and give every exception an expiry

Engineers discussing a checklist around an office table.
Release approval should follow evidence, not the calendar. Photo: NASA Goddard Photo and Video (BY 2.0)

A Kubernetes cgroup v2 migration is GO only when all six gates pass. A booting node is not a release decision.

DecisionRequired evidence or blocker
GOReconciled fleet evidence; reproducible, supported image/runtime combination; workload and agent passes; acceptable cgroup v2 memory monitoring; PDB-respecting drain; successful OS return-path rehearsal; approved overlap cost.
HOLDUnknown custom AMIs, unsupported agents, incorrect heap sizing, blocked recovery, exhausted subnet capacity or inadequate budget. Any one blocks release.

If the kubelet fails to start, investigate the cause. A failCgroupV1 override cannot turn failed compatibility, monitoring or recovery tests green.

Postpone the Kubernetes upgrade if safe operation depends on an untested OS migration. That is defensible only while the current release remains within an acceptable security and provider-support window. Give every HOLD a named owner, remediation date, explicit expiry and support-cost review. Expiry triggers reassessment, not automatic approval or indefinite delay.

If the current platform is already exposed, agree a separately tested emergency path. Do not compress fleet discovery and an EKS AL2023 migration into the upgrade window.

Keep the two rollback decisions separate

Node-pool rollback restores the previous node image and capacity. Control-plane rollback is a separate operation: current EKS documentation allows eligible rollback to the previous minor within seven days, subject to prerequisites and compatibility checks. It is not an unconditional downgrade and does not replace the OS return-path test.

Bring this evidence to your cluster upgrade-readiness assessment:

  • Fleet inventory, AMI provenance and reproducible build records.
  • Workload, agent, heap-sizing and memory-monitoring results.
  • Drain and return-path logs; subnet headroom; approved overlap budget.
  • Exception owners, expiry dates and support-cost decisions.

For a second pair of eyes on your Kubernetes upgrade checklist, talk to a cloud engineer.

Final thoughts

A Kubernetes cgroup v2 migration is ready when the evidence agrees: the fleet is accounted for, the replacement image is reproducible, applications recognise their limits, monitoring remains useful and workloads survive both the move and the return journey. If an essential gate is still unknown, separate the OS work from the version upgrade rather than gambling on both. Cloudline Consulting can help turn those unknowns into a practical plan. Talk to a cloud engineer about a cluster upgrade-readiness assessment.

Frequently asked questions

Why does kubelet fail to start after a Kubernetes upgrade?

One possible cause is a Linux node still using cgroup v1. From Kubernetes v1.35, failCgroupV1 defaults to true upstream, so kubelet rejects that environment at startup. Check the kubelet journal and the host’s cgroup mounts before changing configuration. Runtime or cgroup-driver problems can also require investigation.

How do I check whether Kubernetes nodes use cgroup v2?

Run stat -fc %T /sys/fs/cgroup/ on each Linux host. The expected unified-v2 result is cgroup2fs. Investigate other results with the mount layout rather than inferring compatibility from the OS name. For fleet visibility, collect kubelet_cgroup_version and explicitly flag nodes whose metrics cannot be retrieved.

Can I set failCgroupV1 to false instead of migrating?

Upstream Kubernetes retains that temporary escape hatch, but it does not make every operating-system or provider configuration supported. AL2023 running cgroup v1 is specifically unsupported. Record any exception with its owner, expiry and tested recovery path. KEP-5573 gives no fixed removal date, only an earliest boundary of v1.38.

Does moving EKS nodes to AL2023 require a new node group?

For managed node groups using standard launch templates, or custom templates without an AMI ID, AWS requires blue/green migration. Custom-AMI launch-template paths have different update mechanics. A separate AL2023 canary group is still the recommended approach here because it preserves an independently testable old pool while you validate workloads and bootstrap configuration.

Why can memory monitoring change after moving to cgroup v2?

Do not assume every memory series measures application heap. cAdvisor’s working-set model includes recently accessed and kernel memory, and node-level accounting differences have been documented between cgroup versions. Compare equivalent loads, raw cgroup values, application memory detection and OOM events before changing limits or concluding that the application has developed a leak.

Sources and further reading

Found this useful? Share it or cite it: