
Photo by Brett Sayles on Pexels
An EKS node group version is outdated when DescribeNodegroup returns a Kubernetes version older than the cluster’s version from DescribeCluster. EKS won’t upgrade the control plane until managed nodes match it. Add-ons go stale the same way: compare DescribeAddon‘s addonVersion with the default and latest compatible versions from DescribeAddonVersions.
Cluster upgrades stall on the parts nobody looks at: a node group left one or two versions behind, an Amazon Linux 2 AMI that no longer gets updates, or a VPC CNI add-on that was never updated after the last control plane upgrade. This example is for platform engineers who want one report of every place where an EKS node group version is outdated, across clusters and Regions, before planning the next upgrade.
The TypeScript script for the AWS SDK for JavaScript v3 reads clusters, managed node groups, EKS add-ons and open upgrade insights. It changes nothing; updates are listed as next steps. For clusters whose control plane is already past standard support, pair it with the script to find EKS clusters on extended support, which covers the cost side.
Why is an EKS node group version outdated?
Three rules decide when a node group counts as behind:
- Nodes can’t be newer than the control plane. The version you update a node group to can’t be greater than the control plane’s version.
- The next control plane upgrade needs matching nodes. The EKS cluster upgrade guide says that before you update the control plane, managed and Fargate nodes must be on the same Kubernetes minor version as the control plane. EKS also upgrades the control plane one minor version at a time, so nodes have to catch up before every step.
- Kubelet skew has a hard limit. The upstream Kubernetes version skew policy allows the kubelet to be up to three minor versions older than
kube-apiserver, never newer.
A node group can also be on the right Kubernetes version and still be outdated at the AMI level. Each managed node group has a releaseVersion, and updating it moves nodes to the latest AMI release for that Kubernetes version, with its security patches. Node groups on Amazon Linux 2 are a special case: EKS stopped publishing EKS-optimized AL2 AMIs on 26 November 2025, and Kubernetes 1.32 was the last version with AL2 AMIs. Those node groups have to move to AL2023 or Bottlerocket before they can follow the cluster past 1.32.
How do EKS add-on versions fall behind?
EKS add-ons such as vpc-cni, coredns and kube-proxy don’t update themselves when you upgrade the cluster. After a control plane upgrade, AWS’s procedure is to update nodes, then update the VPC CNI, CoreDNS and kube-proxy add-ons. DescribeAddonVersions lists each add-on version with the cluster versions it’s compatible with, and marks one defaultVersion per Kubernetes version. The script reports three states: installed version not listed as compatible with the cluster version, older than the default, or older than the newest compatible version.
What does the script do?
- Reads clusters
paginateListClustersandDescribeClustergive the Kubernetes version, the upgrade policysupportTypeand cluster health issues. - Counts open upgrade insights
paginateListInsightswith categoryUPGRADE_READINESSand statusERRORorWARNING, such as deprecated API usage that blocks the next version. - Checks managed node groups
DescribeNodegroupreturnsversion,releaseVersion,amiType,statusand health issues. The script calculates the minor-version skew and flags AL2 and custom AMIs. - Checks add-ons
DescribeAddonfor the installed version, andpaginateDescribeAddonVersions(cached per add-on and Kubernetes version) for the default and newest compatible versions.
Prerequisites
- Node.js 18 or later, npm,
tsxand@aws-sdk/client-eks. - A read-only profile. This uses the EKS API only, so it needs no Kubernetes RBAC access and no
kubeconfig. - For accounts with many clusters, the guide to configure retry and timeout settings in AWS SDK v3 helps if you hit throttling.
Which IAM permissions does it need?
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadEksVersions",
"Effect": "Allow",
"Action": [
"eks:ListClusters",
"eks:DescribeCluster",
"eks:ListInsights",
"eks:ListNodegroups",
"eks:DescribeNodegroup",
"eks:ListAddons",
"eks:DescribeAddon",
"eks:DescribeAddonVersions"
],
"Resource": "*"
}
]
}
All eight actions are read-only. The free IAM policy generator for TypeScript code produces the list from the script, and you can scope it to cluster ARNs if you prefer.
The script to find outdated EKS node groups and add-ons
// find-outdated-eks-node-groups-and-add-ons.ts
// For every EKS cluster: managed node groups behind the control plane version, node groups on
// Amazon Linux 2 or custom AMIs, add-ons older than the default or latest compatible version,
// health issues, and open upgrade insights. Report only: it never updates anything.
// Usage:
// npx tsx find-outdated-eks-node-groups-and-add-ons.ts [--regions us-east-1,eu-west-1]
import {
DescribeAddonCommand,
DescribeClusterCommand,
DescribeNodegroupCommand,
EKSClient,
paginateDescribeAddonVersions,
paginateListAddons,
paginateListClusters,
paginateListInsights,
paginateListNodegroups,
} from "@aws-sdk/client-eks";
const args = process.argv.slice(2);
const flag = (name: string): string | undefined => {
const i = args.indexOf(name);
return i >= 0 ? args[i + 1] : undefined;
};
const regions = (flag("--regions") ?? process.env.AWS_REGION ?? "us-east-1")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
interface ClusterRow { Region: string; Cluster: string; Version: string; Support: string; UpgradeInsights: string; Health: string }
interface NodegroupRow { Region: string; Cluster: string; Nodegroup: string; Version: string; Release: string; AMI: string; Findings: string }
interface AddonRow { Region: string; Cluster: string; Addon: string; Installed: string; Default: string; Latest: string; Findings: string }
const minor = (v: string | undefined): number => Number((v ?? "").split(".")[1] ?? NaN);
// "v1.19.2-eksbuild.1" -> [1, 19, 2, 1]; compares numerically, part by part.
function compareVersions(a: string, b: string): number {
const pa = (a.match(/\d+/g) ?? []).map(Number);
const pb = (b.match(/\d+/g) ?? []).map(Number);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
if (d !== 0) return d;
}
return 0;
}
// Default and newest add-on versions for one Kubernetes version, cached per region.
async function addonTargets(eks: EKSClient, cache: Map<string, { def: string; latest: string; all: Set<string> }>, addon: string, k8s: string) {
const key = `${addon}@${k8s}`;
const hit = cache.get(key);
if (hit) return hit;
let def = "";
const all = new Set<string>();
for await (const page of paginateDescribeAddonVersions({ client: eks }, { addonName: addon, kubernetesVersion: k8s })) {
for (const info of page.addons ?? []) {
for (const v of info.addonVersions ?? []) {
const compat = v.compatibilities?.find((c) => c.clusterVersion === k8s);
if (!compat || !v.addonVersion) continue;
all.add(v.addonVersion);
if (compat.defaultVersion) def = v.addonVersion;
}
}
}
const latest = [...all].sort(compareVersions).pop() ?? "";
const result = { def, latest, all };
cache.set(key, result);
return result;
}
async function scanRegion(region: string, clusters: ClusterRow[], nodegroups: NodegroupRow[], addons: AddonRow[]): Promise<void> {
const eks = new EKSClient({ region });
const cache = new Map<string, { def: string; latest: string; all: Set<string> }>();
for await (const page of paginateListClusters({ client: eks }, {})) {
for (const name of page.clusters ?? []) {
const { cluster } = await eks.send(new DescribeClusterCommand({ name }));
const k8s = cluster?.version ?? "?";
const insightCounts = new Map<string, number>();
for await (const ip of paginateListInsights({ client: eks }, { clusterName: name, filter: { categories: ["UPGRADE_READINESS"], statuses: ["ERROR", "WARNING"] } })) {
for (const i of ip.insights ?? []) {
const s = i.insightStatus?.status ?? "UNKNOWN";
insightCounts.set(s, (insightCounts.get(s) ?? 0) + 1);
}
}
clusters.push({
Region: region,
Cluster: name,
Version: k8s,
Support: cluster?.upgradePolicy?.supportType ?? "?",
UpgradeInsights: [...insightCounts].map(([s, n]) => `${n} ${s}`).join(", ") || "none open",
Health: (cluster?.health?.issues ?? []).map((i) => i.code).join(", ") || "ok",
});
for await (const ngPage of paginateListNodegroups({ client: eks }, { clusterName: name })) {
for (const ngName of ngPage.nodegroups ?? []) {
const { nodegroup: ng } = await eks.send(new DescribeNodegroupCommand({ clusterName: name, nodegroupName: ngName }));
const findings: string[] = [];
const skew = minor(k8s) - minor(ng?.version);
if (skew > 0) findings.push(`${skew} minor behind the control plane, blocks the next cluster upgrade`);
if (skew > 3) findings.push("outside the kubelet skew policy");
const ami = ng?.amiType ?? "?";
if (ami.startsWith("AL2_")) findings.push("Amazon Linux 2 AMI, no EKS AMI updates since 26 Nov 2025");
if (ami === "CUSTOM") findings.push("custom AMI, update through a new launch template version");
if (ng?.status && ng.status !== "ACTIVE") findings.push(`status ${ng.status}`);
for (const issue of ng?.health?.issues ?? []) findings.push(`health ${issue.code}`);
nodegroups.push({
Region: region,
Cluster: name,
Nodegroup: ngName,
Version: ng?.version ?? "?",
Release: ng?.releaseVersion ?? "?",
AMI: ami,
Findings: findings.join("; ") || "ok",
});
}
}
for await (const adPage of paginateListAddons({ client: eks }, { clusterName: name })) {
for (const addonName of adPage.addons ?? []) {
const { addon } = await eks.send(new DescribeAddonCommand({ clusterName: name, addonName }));
const installed = addon?.addonVersion ?? "?";
const target = await addonTargets(eks, cache, addonName, k8s);
const findings: string[] = [];
if (target.all.size > 0 && !target.all.has(installed)) findings.push(`not listed as compatible with ${k8s}`);
if (target.def && compareVersions(installed, target.def) < 0) findings.push("older than the default version");
else if (target.latest && compareVersions(installed, target.latest) < 0) findings.push("newer version available");
if (addon?.status && addon.status !== "ACTIVE") findings.push(`status ${addon.status}`);
for (const issue of addon?.health?.issues ?? []) findings.push(`health ${issue.code}`);
addons.push({
Region: region,
Cluster: name,
Addon: addonName,
Installed: installed,
Default: target.def || "?",
Latest: target.latest || "?",
Findings: findings.join("; ") || "ok",
});
}
}
}
}
}
async function main(): Promise<void> {
const clusters: ClusterRow[] = [];
const nodegroups: NodegroupRow[] = [];
const addons: AddonRow[] = [];
for (const region of regions) {
try {
await scanRegion(region, clusters, nodegroups, addons);
} catch (err) {
console.error(`${region}: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`);
}
}
console.log(`Clusters: ${clusters.length}`);
if (clusters.length) console.table(clusters);
console.log(`Managed node groups: ${nodegroups.length}, needing attention: ${nodegroups.filter((n) => n.Findings !== "ok").length}`);
if (nodegroups.length) console.table(nodegroups);
console.log(`Add-ons: ${addons.length}, needing attention: ${addons.filter((a) => a.Findings !== "ok").length}`);
if (addons.length) console.table(addons);
console.log("Report only. Use UpdateNodegroupVersion and UpdateAddon after testing.");
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
How do you run it?
npm install @aws-sdk/client-eks
npm install --save-dev tsx typescript @types/node
AWS_PROFILE=readonly npx tsx find-outdated-eks-node-groups-and-add-ons.ts --regions us-east-1,eu-central-1
Sample output
Clusters: 1
┌─────────┬─────────────┬─────────┬─────────┬────────────┬──────────────────────┬────────┐
│ (index) │ Region │ Cluster │ Version │ Support │ UpgradeInsights │ Health │
├─────────┼─────────────┼─────────┼─────────┼────────────┼──────────────────────┼────────┤
│ 0 │ 'us-east-1' │ 'prod' │ '1.34' │ 'STANDARD' │ '1 ERROR, 1 WARNING' │ 'ok' │
└─────────┴─────────────┴─────────┴─────────┴────────────┴──────────────────────┴────────┘
Managed node groups: 2, needing attention: 1
┌─────────┬─────────────┬─────────┬───────────┬─────────┬───────────────────┬──────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ (index) │ Region │ Cluster │ Nodegroup │ Version │ Release │ AMI │ Findings │
├─────────┼─────────────┼─────────┼───────────┼─────────┼───────────────────┼──────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 0 │ 'us-east-1' │ 'prod' │ 'general' │ '1.34' │ '1.34.1-20270902' │ 'AL2023_x86_64_STANDARD' │ 'ok' │
│ 1 │ 'us-east-1' │ 'prod' │ 'legacy' │ '1.32' │ '1.32.3-20251117' │ 'AL2_x86_64' │ '2 minor behind the control plane, blocks the next cluster upgrade; Amazon Linux 2 AMI, no EKS AMI updates since 26 Nov 2025; health AsgInstanceLaunchFailures' │
└─────────┴─────────────┴─────────┴───────────┴─────────┴───────────────────┴──────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Add-ons: 2, needing attention: 2
┌─────────┬─────────────┬─────────┬───────────┬──────────────────────┬──────────────────────┬──────────────────────┬──────────────────────────────────┐
│ (index) │ Region │ Cluster │ Addon │ Installed │ Default │ Latest │ Findings │
├─────────┼─────────────┼─────────┼───────────┼──────────────────────┼──────────────────────┼──────────────────────┼──────────────────────────────────┤
│ 0 │ 'us-east-1' │ 'prod' │ 'vpc-cni' │ 'v1.19.2-eksbuild.1' │ 'v1.20.1-eksbuild.1' │ 'v1.20.4-eksbuild.2' │ 'older than the default version' │
│ 1 │ 'us-east-1' │ 'prod' │ 'coredns' │ 'v1.12.4-eksbuild.1' │ 'v1.12.4-eksbuild.1' │ 'v1.12.6-eksbuild.1' │ 'newer version available' │
└─────────┴─────────────┴─────────┴───────────┴──────────────────────┴──────────────────────┴──────────────────────┴──────────────────────────────────┘
Report only. Use UpdateNodegroupVersion and UpdateAddon after testing.
Names and version numbers are illustrative. The legacy node group blocks the move to the next Kubernetes version twice over: it’s two versions behind, and its AL2 AMI type has no AMIs for versions after 1.32.
How do you update an outdated node group or add-on?
- Clear the upgrade insights firstEKS refreshes an insight 24 hours after its last refresh, and deprecated API usage is measured over a rolling 30-day window, so an
ERRORcan linger after you fix it. - Update node groups on EKS-optimized AMIs
aws eks update-nodegroup-version --cluster-name prod --nodegroup-name generalmoves the node group to the cluster’s Kubernetes version and the latest AMI release for it. A rolling update respects Pod disruption budgets and fails if it can’t drain a node; a forced update ignores them. - Replace AL2 and custom AMI node groupsCustom AMIs update through a new launch template version. AL2 node groups need a new AL2023 or Bottlerocket node group, then a drain of the old one.
- Update add-ons
aws eks update-addon --cluster-name prod --addon-name vpc-cni --addon-version v1.20.1-eksbuild.1 --resolve-conflicts PRESERVEkeeps values you changed on the cluster; AWS recommends testing that on a non-production cluster first. Update one add-on at a time and watch its health.
Version drift isn’t only an EKS problem. The same pattern shows up when you check RDS auto minor version upgrade and pending maintenance and when you find Lambda functions on deprecated runtimes.
Troubleshooting
- A node group is missing from the report.
ListNodegroupsonly returns managed node groups. Self-managed nodes, Karpenter nodes and EKS Auto Mode nodes don’t appear; compare withkubectl get nodes. - An add-on shows
?for the default. It’s installed with Helm orkubectlrather than as an EKS add-on, or it’s a community or Marketplace add-on with no default for that version. - Skew says behind, but the console shows no update. Custom AMI node groups don’t get console update notices. Build a new AMI and launch template version.
AccessDeniedExceptiononListInsights. Addeks:ListInsights; the guide to troubleshoot AWS IAM access denied errors helps with SCP denials.
Ask ChatWithCloud instead
Ask ChatWithCloud “Which EKS node groups in us-east-1 are behind their cluster version?” It writes AWS SDK for JavaScript v2 code, runs it with your profile and summarizes the answer. SDK v2 reached end of support on 8 September 2025, so services and features launched after that date may be missing from its answers; use the script above for upgrade insights. The how ChatWithCloud works page explains the loop, and the guide to list AWS resources with natural language from your terminal has more examples.
Frequently asked questions
How do I check the version of an EKS node group?
Run aws eks describe-nodegroup --cluster-name prod --nodegroup-name general and read version (Kubernetes) and releaseVersion (AMI release).
Can an EKS node group be on an older version than the cluster?
Yes, up to three minor versions under the Kubernetes skew policy, but EKS requires managed nodes to match the control plane before you can upgrade the control plane again.
Do EKS add-ons update automatically after a cluster upgrade?
No. After the control plane and nodes, AWS’s upgrade procedure has you update the VPC CNI, CoreDNS and kube-proxy add-ons yourself.
Can I still use Amazon Linux 2 EKS nodes?
Existing AL2 nodes keep running, but EKS stopped publishing AL2 AMIs on 26 November 2025 and 1.32 was the last Kubernetes version with them. Plan a move to AL2023 or Bottlerocket.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud