Three tools will tell you whether an EKS cluster is safe to upgrade. They disagree, and the
disagreement is the useful part.
On December 2, 2026, Kubernetes 1.34 leaves Amazon EKS standard support.
AWS does not make you opt in to what happens next:
"By default, for all new and existing clusters, the upgrade policy is set to EXTENDED."
Every 1.34 cluster still on that default moves to extended support that day. The control plane
bill goes from $0.10 per cluster-hour to $0.60. Billing starts at the beginning of the day the
version reaches end of standard support, UTC.
Nothing breaks. Nothing pages anyone. A line item changes.
This is not really an article about a date. It is about the question the date forces — is this
cluster actually safe to upgrade? — and the fact that the three tools you reach for to answer
it will give you three different answers.
The question is not which one is lying. It is what each one is actually measuring.
AWS does warn you about the date, and it is worth being accurate about that. EKS posts a notice
to the AWS Health Dashboard roughly 12 months after a version was released on EKS, at least 60
days before its end-of-standard-support date.
Nobody can say they were not told. But the notice goes to a console most teams do not have open,
about a date that costs nothing until it passes. The signal that reliably gets read is the
invoice.
So this is about the part before the invoice. How you find out whether a cluster is safe to
upgrade, why the three available answers disagree, and how to read the disagreement instead of
guessing.
Most of it is AWS CLI and kubectl. The steps where that gets tedious are marked as tedious.
There is a tool at the end, and you do not need it.
If you run EKS and you have not opened your cluster's upgrade insights this quarter, that is the
one thing here worth doing before you close the tab.
The Clock
The EKS version lifecycle is fixed and public:
- A minor version gets 14 months of standard support from its EKS release date.
- Then 12 months of extended support, automatically, at an added cost per cluster hour.
- At the end of those 26 months, AWS auto-upgrades your control plane. You get no advance notification of when, and AWS explicitly declines to give a time frame.
Read that last point twice if you run anything regulated.
The auto-upgrade covers the control plane only. Self-managed nodes and EKS managed node groups
stay where they are — EKS Auto Mode nodes, in AWS's words, "may automatically update." An
unattended cluster at end-of-life gets a one-sided version bump and a skew you did not plan.
The current calendar, from the AWS docs (Kubernetes version
lifecycle, checked
2026-10-21), as version — end of standard support / end of extended support:
- 1.37 — 2027-12-01 / 2028-12-01
- 1.36 — 2027-08-02 / 2028-08-02
- 1.35 — 2027-03-27 / 2028-03-27
- 1.34 — 2026-12-02 / 2027-12-02
- 1.33 — 2026-07-29, already extended / 2027-07-29
- 1.32 — 2026-03-23, already extended / 2027-03-23
- 1.31 — 2025-11-26, already extended / 2026-11-26
You can pull this yourself, per account, without clicking through the console:
aws eks describe-cluster-versions \
--query 'clusterVersions[].[clusterVersion,versionStatus,endOfStandardSupportDate,endOfExtendedSupportDate]' \
--output table
(There is also a status field. It is deprecated — the CLI's own help says "This field is
deprecated. Use versionStatus instead." The values differ in case: versionStatus returns
STANDARD_SUPPORT, EXTENDED_SUPPORT, UNSUPPORTED.)
What the Delay Actually Costs
From the EKS pricing page (checked 2026-10-21):
- Standard support: $0.10 per cluster per hour
- Extended support: $0.60 per cluster per hour — the standard $0.10 plus a $0.50 surcharge
The surcharge is the number that matters, because you were paying the $0.10 anyway. Do the
arithmetic out loud so nobody has to trust a rounded figure:
$0.50/hour × 730 hours ≈ $365 per cluster per month
$0.50/hour × 8,760 hours ≈ $4,380 per cluster per year
Per cluster. If a dev, a staging and a prod cluster all drift past the date together — and they
usually do, because they were all created from the same module on the same afternoon — that is
roughly $1,095 a month.
Pure deferral cost. It buys nothing but the right to not upgrade yet.
This is the cheapest honest argument for upgrade hygiene you will ever get to make to a finance
team. Most platform teams never make it, because nobody has put a date and a number next to a
cluster name.
Why "We Checked Our Manifests" Is Not an Answer
Here is the part that surprises people.
The Kubernetes API server stores one copy of each object and converts it on read. A deprecated
version keeps serving right up until it is removed, and while it serves, it returns
everything.
A current example. resource.k8s.io/v1beta1 was deprecated in Kubernetes 1.35 and is not removed
until 1.38, and resource.k8s.io/v1 has been available since 1.34.
So on a 1.36 cluster, kubectl get resourceclaims.v1beta1.resource.k8s.io returns every
ResourceClaim you have — including the ones written last week as v1. The old apiVersion still
serves. It just does not mean anything about how the object was written.
The famous version of this is the one that taught everyone the lesson: before 1.22, listing
Ingresses through extensions/v1beta1 returned Ingresses that had been written as
networking.k8s.io/v1. That particular version is long gone — no supported EKS version serves
it — but the mechanism is not historical. It runs again every time a beta graduates.
So a tool that reports everything it can list through a deprecated version produces false
positives, confidently, at scale.
And a tool that only reads your Git manifests misses everything written by a controller, an
operator, a Helm chart you did not render, or a colleague with kubectl.
The honest way to ask "was this object written using a removed API?" is to look for evidence of
how it was last written:
- the
apiVersionrecorded in thekubectl.kubernetes.io/last-applied-configurationannotation, and - the newest
managedFieldsentry for the main resource, by timestamp — ignoring entries for subresources likestatus, which controllers rewrite with the current version.
That technique is not new. kube-no-trouble
(kubent) has used the last-applied annotation for
years, and tideclear credits it as the reference.
What matters is the consequence: objects with no such evidence cannot be judged. An object
written before Kubernetes 1.18, or by a client that leaves neither annotation nor managedFields,
is unknowable from inside the cluster. The correct output for those is a count of "I could not
check these," not a green tick.
If your readiness report has no "unverified" number in it, it is not telling you the truth about
its own coverage.
The Third Source of Truth, and Why It Disagrees
EKS ships its own answer: upgrade insights. Every EKS cluster is scanned automatically
against an AWS-curated list of upgrade-impacting checks, refreshed roughly every 24 hours, with
an on-demand refresh button in the console
(docs, checked
2026-10-20).
The deprecated-API insight is the interesting one, because it sees something no in-cluster
scanner can see: actual API calls, from audit logs.
That is a genuinely better signal in one specific way. Your cluster's current objects tell you
what exists. The audit log tells you what is still calling.
The CI job that applies a v1beta1 manifest every deploy. The operator with a stale client
library. The person with a five-year-old kubeconfig. As a maintainer who lists AWS as their
employer put it in the public roadmap thread, the risk is not only what is in the cluster now:
"It's anything that gets deployed after the cluster is upgraded."
And then it has the behaviour that generates the support tickets.
The 30-Day Ghost
AWS builds the deprecated-API insight from a rolling 30-day lookback over audit logs.
So when you find the offending workload, fix it, redeploy it, and refresh the insight — the
insight stays red. For up to 30 days. Because a call that happened 29 days ago is still inside
the window.
This is not a bug, and it is worth understanding rather than cursing. AWS has stated it is
deliberate ([aws/containers-roadmap
2569](https://github.com/aws/containers-roadmap/issues/2569)):
"This was an intentional (although debated) product decision. We decided the only foolproof
way to safely detect deprecated API usage in Kubernetes is a rolling look back window through
audit logs."
A shorter window would clear faster. It would also miss the once-a-fortnight deploy job that is
the exact failure mode the check exists to catch.
Correct design. Miserable ergonomics.
The result is that a team doing the right thing — fixing the deprecation promptly — gets a red
dashboard for a month, and the dashboard cannot distinguish them from a team that has done
nothing at all.
So people route around it. They decide the insight is noise, or they wait out the month and lose
the upgrade window to the next quarter's change freeze. Both are worse than reading the data
properly.
Reading a Red Insight Honestly, by Hand
You can resolve almost every one of these yourself. The insight detail carries the timestamps you
need — AWS surfaces the last call time per client precisely so you can gain confidence while the
status is still red.
1. Pull the insight detail.
aws eks list-insights --cluster-name <cluster> \
--filter 'categories=UPGRADE_READINESS' \
--query 'insights[].[id,name,insightStatus.status]' --output table
aws eks describe-insight --cluster-name <cluster> --id <insight-id>
Look at lastRefreshTime, and inside the category-specific detail, the deprecated API versions
with their client user agents, request counts, and last request time.
2. Ask the three questions in order. For each flagged apiVersion:
- Does any live object still carry evidence of it? Check the last-applied annotation and the newest main-resource managedFields entry — not what the API server will happily list for you through the old version. If yes: it is a real blocker. Stop here. Nothing about the 30-day window matters.
-
Has a client called it recently? "Recently" has to be measured from the insight's
lastRefreshTime, not from now — the call timestamps are only as fresh as the refresh that produced them. A call six days before the refresh is six days old as the insight sees it (nine days old counted from today), which is inside a seven-day window, so it still counts as active. And a client entry with no call timestamp at all counts as still calling: you cannot date it, so you cannot age it out. If a client is still calling, it is a real blocker, and the user agent usually names the culprit. - Neither? Then it is a ghost. It clears on last call + 30 days. Put that date in the calendar and move on.
3. Know when you cannot answer. You have not proved it clean if any of these is true:
- the live read of that resource failed or was denied by RBAC;
- some objects of that resource had no apiVersion evidence;
- the insight carries no client call dates at all to judge by;
-
lastRefreshTimeis missing, stale, or somehow in the future.
The right label there is "unknown," and unknown must never be treated as clean.
That distinction — ghost versus unknown — is the whole game. Collapsing them is how a readiness
check gets a reputation for lying.
The Things That Break Upgrades That Nobody Checks
Deprecated APIs get all the attention and are, in my experience, the second most common reason
an EKS upgrade goes badly. The others:
Nodes that cannot follow the control plane. Two deadlines have already passed. The
consequence is still ahead of you.
- EKS stopped publishing EKS-optimized Amazon Linux 2 AMIs on 26 November 2025. Kubernetes 1.32 was the last version it published them for.
- Amazon Linux 2 itself reached end of support on 30 June 2026 (AMI deprecation FAQ, checked 2026-10-20).
- From Kubernetes 1.35, the kubelet's
failCgroupV1defaults to true (KEP-5573) — a kubelet does not start on a cgroup v1 host by default. - AL2 is cgroup v1.
Put those together. An AL2 node group is a survivable warning on a 1.33 or 1.34 target, and a
hard blocker on a 1.35 target.
Check amiType on your managed node groups, and status.nodeInfo.osImage for everything else —
Karpenter nodes, self-managed ASGs, that one instance someone built by hand.
One trap: Amazon Linux 2023 contains the string Amazon Linux 2 and is not AL2. Anchor your
match or you will scare yourself for no reason.
Kubelet version skew. Upstream tests up to three minor versions of skew, and never the other
direction. A node newer than the control plane, or more than three minors older, is a problem
before you add another minor to the gap.
Add-on compatibility. For every EKS-managed add-on, ask EKS directly which versions support
your target version, not your current one:
aws eks describe-addon-versions --addon-name vpc-cni --kubernetes-version <target>
If your installed version is not in that list, you need to move the add-on before, or with, the
control plane. Self-managed add-ons are yours to verify; nothing checks those for you.
Whether you can go back. This one is newer than most upgrade checklists and almost nobody
mentions it.
After you upgrade, EKS emits a second category of insight — ROLLBACK_READINESS — which tells
you whether the cluster can be rolled back to the previous minor version. It is only visible
during the seven-day rollback eligibility window after the upgrade (View cluster
insights, checked
2026-10-21):
aws eks list-insights --cluster-name <cluster> \
--filter '{"categories": ["ROLLBACK_READINESS"], "statuses": ["ERROR"]}'
Read it the day you upgrade, not the day you need it.
Two things to expect. An ERROR here is often normal — "at least one node kubelet version
matches the cluster control plane version" is an error for rollback purposes and a success for
everything else. And an UNKNOWN rollback insight blocks the rollback until EKS can evaluate it
or you pass --force.
Knowing on day one whether your escape hatch is open is worth the single API call.
A Checklist You Can Run Today
For each cluster, before you plan an upgrade:
-
Date and cost. When does standard support end, and what is the monthly surcharge if you
miss it? (
describe-cluster-versions; $0.50/hour ≈ $365/month per cluster.) - Objects. What still carries evidence of a removed apiVersion — from last-applied or newest managedFields, not from what lists through the old version?
- Coverage. How many objects could you not check, and which resources were denied or skipped? Write the number down.
- Callers. What does the EKS deprecated-API insight say, and for each flagged version, is there a live object, a recent caller, or neither?
- Ghosts. For each "neither": last call + 30 days. Calendar it.
- Unknowns. For each one you cannot answer: say so explicitly. Do not round it to green.
- Nodes. Any AL2 node groups or AL2 nodes? Any kubelet skew that gets worse after the bump?
- Add-ons. Does every managed add-on list the target version as compatible?
- Manifests. Scan the repo too, so you do not reintroduce a removed API on the next deploy.
Steps 1, 4, 5 and 8 are each a handful of CLI calls. Steps 2, 3 and 7 are the tedious ones.
There is no single command that answers "which objects carry evidence of a removed apiVersion,"
which is exactly why tooling exists for it.
Budget an afternoon per cluster the first time. That is my own rough estimate, not a measurement,
and it is the real reason this does not get done: unglamorous work, repeated every time a date
moves, and nobody's favourite afternoon.
The Tool
I wrote tideclear because I got tired of doing the
above by hand.
It is a single read-only Go binary, MIT licensed. It never writes to a cluster or to AWS, needs
no server or account, and sends no telemetry. The AWS permissions it needs are List* and
Describe* only, listed in full in the README.
What it does, mapped onto the checklist above:
-
tideclear clusters --all-regions— your clusters with version, support dates, days left, and whether each is already on extended support (step 1).--all-regionswalks a built-in region list rather than discovering regions, and any region that rejects your credentials is reported as skipped rather than silently dropped. -
tideclear scan— a live scan using the evidence rule described above, reporting blockers, warnings, and a count of objects it could not verify (steps 2, 3). -
tideclear scan --path ./manifests --target 1.37— the same rules against a directory. With--target, it needs no cluster and no AWS access at all; with--clusterinstead, it calls EKS to read the current version (step 9). -
tideclear insights --cluster NAME— the cluster's upgrade insights with how long ago EKS refreshed them (step 4). -
tideclear scan --cluster NAME --insights— the join: one row per deprecated API version in the insight, labelledBLOCKER,STALE,OK, orUNKNOWN, with the evidence that produced the label and, forSTALE, the date it clears (steps 4, 5, 6). - Add-on and node checks run as part of
scan --cluster(steps 7, 8).
Before you give it access to anything, there is tideclear demo. It prints a full sample report
— a blocker, a ghost with its clears-on date, an incompatible add-on, an Amazon Linux 2 node
group — from data compiled into the binary. It reads nothing from AWS or Kubernetes, needs no
credentials, and exits 0. I am not going to paste its output here, because the dates in it are
relative to the day you run it; run it and you will see the shape in about two seconds.
Here is the live-scan half of the join, from the README:
$ tideclear scan --cluster demo-a --insights
cluster: demo-ctx (api.example.test) [EKS: demo-a]
KIND APIVERSION NAMESPACE NAME REPLACEMENT REMOVED IN SEVERITY
IPAddress networking.k8s.io/v1beta1 - api networking.k8s.io/v1 1.37 BLOCKER
ServiceCIDR networking.k8s.io/v1beta1 - main networking.k8s.io/v1 1.37 BLOCKER
upgrade insights:
APIVERSION KIND AWS STATUS VERDICT EVIDENCE
networking.k8s.io/v1beta1 IPAddress ERROR BLOCKER object IPAddress api
networking.k8s.io/v1beta1 ServiceCIDR ERROR BLOCKER object ServiceCIDR main
AWS marked both of those ERROR. In this case AWS is right, and the row says why it is right:
there is a live IPAddress named api and a live ServiceCIDR named main that genuinely
carry the removed apiVersion. The EVIDENCE column names the object. That is a blocker you can
act on, rather than a status you have to go and investigate.
The interesting case is the opposite one, and it is the same row shape. The README puts it
plainly:
A row with no live object, whose last client call was more than 7 days before the insight
refreshed, is STALE: AWS still reports it, and clears it on the date the row shows.
Same insight, same red status from AWS, opposite verdict — and the difference between them is
the part you actually needed. If you want to see a STALE row next to a BLOCKER row without
pointing anything at a cluster, that is what tideclear demo prints.
Which of These Four You Should Actually Use
Nothing here is a replacement for anything else. They answer different questions, and for most
readers one of the first three is the right answer.
kube-no-trouble (kubent) — "Easily check your
clusters for use of deprecated APIs." MIT, 3.7k stars, last pushed September 2026. It is where
the last-applied evidence technique comes from; tideclear credits it as the reference.
Best for: any Kubernetes cluster, anywhere. If you are not on EKS, start here and stop here.
Pluto — "A cli tool to help discover deprecated
apiVersions in Kubernetes." Apache-2.0, 2.6k stars, last pushed October 2026. tideclear embeds
its deprecation list, credited in the repo.
Best for: files and charts, before they reach a cluster. The CI check that stops you
reintroducing a removed API on the next deploy.
AWS upgrade insights — free, already on, no install, and the only one of the four that can
see callers rather than objects.
Best for: finding the client nobody knew about. If you only do one thing from this article,
read your insights.
tideclear — the join. Support clock, live scan, the
AWS insight, add-ons and nodes in one report, with the 30-day ghosts labelled as ghosts and the
unknowns labelled as unknowns.
Best for: an EKS cluster whose insight is red when you believe it should be green, and you
want the reason rather than another status.
The gap. Everything that reads a live cluster — the insight, a live scan, kubent, mine — can
only see a workload after it has been applied or has called the API. Nothing on that side of the
line sees one that has not.
The 30-day window is a backward-looking instrument. The live scan is a snapshot of what exists
right now. The one tool here that can see earlier is the manifest scan, and it sees exactly the
files you point it at, which is not the same as everything that will eventually run. So a
quarterly job that calls a removed API, has not fired since your last upgrade, and lives in a
chart nobody scans is invisible to all four until it fires. Pointing a manifest scan at
everything you deploy is the only thing on this list that closes that, and most people have not
got "everything we deploy" in one place to point it at.
And what I will not claim about mine. The insight reconciliation, add-on and node checks are
covered by unit tests against fakes. They have not been validated against a real cluster that
genuinely has a removed-API object or an Amazon Linux 2 node group. The README's Limits section
puts it as "tested against fakes. Validation on a real cluster is still pending." The demo
command is not evidence against that — it prints compiled-in sample data and says so in its own
first line. It proves what the output looks like, not that the detection is right.
Two more limits from that same section, because they are the ones that would waste your
afternoon. Unrendered Helm templates are skipped, not scanned, so run helm template first. And
a deprecated API that a controller calls in a loop stays a BLOCKER and never goes stale, which
is correct and occasionally maddening.
I would rather you read those sentences from me than discover them yourself. If you point it at
a real cluster and it gets something wrong, that is the single most useful thing you could send
me.
Closing Thoughts
The date is the easy part. One CLI call gives it to you, and the surcharge is arithmetic.
The hard part is that "ready to upgrade" is not one question. The three sources that claim to
answer it are measuring three different things — what exists, what was written, and what is
still calling — and the gap between them is not noise.
A readiness check that collapses those three into a single green tick is not being helpful. It
is being confident.
Read the disagreement instead. It is the only part of the output that carries information.
What's Your Take?
There is no waitlist and no mailing list. I would rather have one concrete report than a hundred
addresses, and the repository already takes issues.
What do you actually check by hand before an EKS upgrade? What bit you last time? Is the
thirty-day red status something your team has fought, or something you have never noticed?
And if you run tideclear and it gets something wrong — a BLOCKER that wasn't one, a STALE
date that came and went, an Amazon Linux 2 node group it walked straight past — open an
issue with the row it printed and the command you
ran. Redact the names; I don't need them.
The interesting paths have only ever been exercised against fakes. The first person to point this
at a real cluster will teach me more than the test suite has.

Top comments (0)