{"id":3594,"date":"2026-11-05T09:01:02","date_gmt":"2026-11-05T16:01:02","guid":{"rendered":"https:\/\/yyc.ninja\/?p=3594"},"modified":"2026-11-05T09:01:02","modified_gmt":"2026-11-05T16:01:02","slug":"upgrading-kubernetes-rancher-cert-manager-no-spare-capacity","status":"publish","type":"post","link":"https:\/\/yyc.ninja\/index.php\/2026\/11\/05\/upgrading-kubernetes-rancher-cert-manager-no-spare-capacity\/","title":{"rendered":"Upgrading Kubernetes, Rancher and cert-manager with No Spare Capacity"},"content":{"rendered":"<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"630\" src=\"https:\/\/yyc.ninja\/wp-content\/uploads\/2026\/11\/cg-upgrading-kubernetes-rancher-cert-manager-no-spare-capacity-cover.png\" alt=\"Upgrading Kubernetes, Rancher and cert-manager with No Spare Capacity\" class=\"wp-image-3593\" sizes=\"auto, (max-width: 1200px) 100vw, 1200px\" srcset=\"https:\/\/yyc.ninja\/wp-content\/uploads\/2026\/11\/cg-upgrading-kubernetes-rancher-cert-manager-no-spare-capacity-cover.png 1200w, https:\/\/yyc.ninja\/wp-content\/uploads\/2026\/11\/cg-upgrading-kubernetes-rancher-cert-manager-no-spare-capacity-cover-300x158.png 300w, https:\/\/yyc.ninja\/wp-content\/uploads\/2026\/11\/cg-upgrading-kubernetes-rancher-cert-manager-no-spare-capacity-cover-1024x538.png 1024w, https:\/\/yyc.ninja\/wp-content\/uploads\/2026\/11\/cg-upgrading-kubernetes-rancher-cert-manager-no-spare-capacity-cover-768x403.png 768w, https:\/\/yyc.ninja\/wp-content\/uploads\/2026\/11\/cg-upgrading-kubernetes-rancher-cert-manager-no-spare-capacity-cover-100x53.png 100w\" \/><\/figure>\n<p class=\"wp-block-paragraph\">Rolling upgrades assume spare capacity: drain a node, let its workloads reschedule, upgrade, repeat. This test platform was deliberately built without that margin, so it was a good place to find out what a safe upgrade looks like when the standard procedure cannot work.<\/p>\n<p class=\"wp-block-paragraph\">The cluster started on <strong>Kubernetes 1.33<\/strong> (end of life), <strong>Rancher 2.13<\/strong> and <strong>cert-manager 1.14<\/strong>. Two days later it ran <strong>RKE2 1.36.5, Rancher 2.15.2 and cert-manager 1.21.2<\/strong>, with ingress-nginx replaced by Gateway API along the way. All of this happened with no spare node, no spare memory and the slow shared storage described in &#8220;When Storage Is the Bottleneck&#8221;.<\/p>\n<h3 class=\"wp-block-heading\">Rancher&#8217;s support matrix set the order<\/h3>\n<p class=\"wp-block-paragraph\">Kubernetes minor versions must be upgraded one at a time. Rancher supports only a window of Kubernetes versions, and <strong>Rancher 2.13 stops at 1.34<\/strong>. That dictated the sequence:<\/p>\n<ol class=\"wp-block-list\">\n<li>RKE2 1.33 to 1.34, the highest version Rancher 2.13 allows.<\/li>\n<li>Rancher 2.13 to 2.14.3 to 2.15.2, one minor version per hop.<\/li>\n<li>cert-manager 1.14 to 1.21.2, the current release.<\/li>\n<li>Gateway API cut-over, bringing in Traefik and removing ingress-nginx. RKE2 1.37 removes ingress-nginx, so this had to happen before going further.<\/li>\n<li>RKE2 1.34 to 1.35 to 1.36.<\/li>\n<\/ol>\n<p class=\"wp-block-paragraph\">An <strong>etcd snapshot<\/strong> preceded every step, named after that step (<code>pre-v1.36.5<\/code>, <code>pre-gateway-api<\/code>, <code>pre-rancher-2.15<\/code>). The rollback for a bad control plane upgrade is to restore the snapshot, so each snapshot is that step&#8217;s undo button.<\/p>\n<h3 class=\"wp-block-heading\">Upgrading nodes that cannot be drained<\/h3>\n<p class=\"wp-block-paragraph\">The textbook sequence is cordon, drain, upgrade, uncordon. It fails here for two reasons:<\/p>\n<ul class=\"wp-block-list\">\n<li><strong>There is no room.<\/strong> Workers sat at 91% to 99% of memory requests, so a drained node&#8217;s pods had nowhere to go.<\/li>\n<li><strong>The pods cannot move.<\/strong> local-path volumes are pinned to the node that holds the directory.<\/li>\n<\/ul>\n<p class=\"wp-block-paragraph\">The opposite approach, simply restarting the agent on the new version, had already failed. The agent took 9 minutes to return on the busy disk, and after 5 minutes in <code>NotReady<\/code>, Kubernetes evicted every pod on the node.<\/p>\n<p class=\"wp-block-paragraph\">The upgrade script that worked was designed around one constraint: <strong>keep the restart inside the 5-minute eviction window<\/strong>.<\/p>\n<ol class=\"wp-block-list\">\n<li>Take an etcd snapshot (server nodes only).<\/li>\n<li><strong>Pre-pull the new release&#8217;s system images while the old version is still running.<\/strong> Image pulls were most of the downtime, so doing them early removes them from the critical path.<\/li>\n<li>Pin settings whose defaults change in the new version. RKE2 1.36 defaults new clusters to Traefik, so <code>ingress-controller<\/code> was set explicitly before upgrading.<\/li>\n<li>Run the install script, use <code>systemctl restart --no-block<\/code>, and wait for Ready.<\/li>\n<\/ol>\n<p class=\"wp-block-paragraph\">Nodes returned in 357, 272 and 182 seconds on the 1.35 upgrade, and in 145, 123 and 181 seconds on 1.36. <strong>No pod was evicted.<\/strong> Pre-pulling cut a 9-minute restart to about 2 to 3 minutes by the final hop.<\/p>\n<h3 class=\"wp-block-heading\">Rancher: upgrades that take an hour<\/h3>\n<p class=\"wp-block-paragraph\">Rancher was the slowest component by a wide margin:<\/p>\n<ul class=\"wp-block-list\">\n<li><strong><code>--reuse-values<\/code> breaks across minor versions.<\/strong> Rancher 2.14 added a value (<code>networkExposure<\/code>) that the old release&#8217;s stored values lack, and the template dereferenced it: <code>nil pointer evaluating .Values.networkExposure.type<\/code>. Use <code>--reset-then-reuse-values<\/code> (Helm 3.14 or later), which applies the new chart&#8217;s defaults and then your overrides.<\/li>\n<li><strong>Startup probes need the worst-case budget.<\/strong> The new pod pulled a 913 MB image in 11 minutes, then restored its chart repositories for more than 15 minutes, and its startup probe killed it twice. The fix was a 60-minute <code>startupProbe<\/code> and <code>helm upgrade<\/code> without <code>--wait<\/code>. The old pod keeps serving until the new one is Ready.<\/li>\n<li><strong>The jail timeout.<\/strong> At startup, Rancher copies binaries into a jail with a 60-second budget, which failed on this storage with <code>[FATAL] timed out<\/code>. Rancher settings can be overridden through environment variables, so <code>CATTLE_JAILER_TIMEOUT=900<\/code> solved it.<\/li>\n<li><strong>System charts that never upgraded.<\/strong> After the upgrade, Fleet, the webhook and Turtles were still on the old chart line. Rancher had tried to install them before its chart cache loaded and never retried. Patching the <code>rancher-charts<\/code> ClusterRepo with a new <code>forceUpdate<\/code> timestamp re-ran the installs. <code>helm list -A<\/code> is now a required check after every Rancher hop.<\/li>\n<\/ul>\n<h3 class=\"wp-block-heading\">Gateway API cut-over without downtime<\/h3>\n<p class=\"wp-block-paragraph\">ingress-nginx and Traefik ran <strong>side by side<\/strong> during the switch, using RKE2&#8217;s <code>ingress-controller: [ingress-nginx, traefik]<\/code>. Traefik had its own MetalLB address and no hostPorts, so nothing conflicted on ports 80 and 443.<\/p>\n<p class=\"wp-block-paragraph\">Each application received an HTTPRoute and was tested on the new address. DNS was then switched, and ingress-nginx was removed by dropping it from the list. Rancher&#8217;s native Gateway mode (<code>networkExposure.type: gateway<\/code>) replaced its Ingress with its own Gateway and routes.<\/p>\n<p class=\"wp-block-paragraph\">One detail to watch: Traefik&#8217;s chart takes the Service type from <code>service.spec.type<\/code>, not <code>service.type<\/code>. The Service stayed ClusterIP until this was corrected.<\/p>\n<h3 class=\"wp-block-heading\">A repeatable upgrade process<\/h3>\n<ol class=\"wp-block-list\">\n<li>Read the support matrix for <strong>every<\/strong> component that pins the Kubernetes version, including Rancher, the CNI and cert-manager, and plan the hops.<\/li>\n<li><strong>Take one etcd snapshot per step,<\/strong> named after the step, and copy it off the node.<\/li>\n<li><strong>Pre-pull images<\/strong> before restarting anything, and measure node downtime against the eviction timeout.<\/li>\n<li>Pin settings whose defaults change, using the changed-defaults section of each release note.<\/li>\n<li>Use <code>--reset-then-reuse-values<\/code>, not <code>--reuse-values<\/code>, across chart minor versions.<\/li>\n<li>For slow applications, upgrade without <code>--wait<\/code>, watch the new pod, and size startup probes for the worst case.<\/li>\n<li>After the last hop, <strong>audit as well as verify<\/strong>: run the functional test suite (79 checks here) and look for inherited state, labels and credentials.<\/li>\n<li>As a next improvement, replace the script with system-upgrade-controller Plans in Git.<\/li>\n<\/ol>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Get the next one in your inbox<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Enjoyed this? I write about local AI, self-hosting, and cloud cost ninjutsu. Join the Ninja Dispatch for occasional, high-signal notes. No fluff.<\/p>\n\n\n\n\t\t\t<form id=\"sib_signup_form_2\" method=\"post\" class=\"sib_signup_form\">\n\t\t\t\t<div class=\"sib_loader\" style=\"display:none;\"><img\n\t\t\t\t\t\t\tsrc=\"https:\/\/yyc.ninja\/wp-includes\/images\/spinner.gif\" alt=\"loader\"><\/div>\n\t\t\t\t<input type=\"hidden\" name=\"sib_form_action\" value=\"subscribe_form_submit\">\n\t\t\t\t<input type=\"hidden\" name=\"sib_form_id\" value=\"2\">\n                <input type=\"hidden\" name=\"sib_form_alert_notice\" value=\"Please fill out all required fields.\">\n                <input type=\"hidden\" name=\"sib_form_invalid_email_notice\" value=\"Please enter a valid email address.\">\n                <input type=\"hidden\" name=\"sib_security\" value=\"ea26a616b7\">\n\t\t\t\t<div class=\"sib_signup_box_inside_2\">\n\t\t\t\t\t<div style=\"\/*display:none*\/\" class=\"sib_msg_disp\">\n\t\t\t\t\t<\/div>\n                    \t\t\t\t\t<p class=\"sib-email-area\">\n    <input type=\"email\" class=\"sib-email-area\" name=\"email\" placeholder=\"you@example.com\" required=\"required\">\n<\/p>\n<p class=\"sib-FIRSTNAME-area\">\n    <input type=\"text\" class=\"sib-FIRSTNAME-area\" name=\"FIRSTNAME\" placeholder=\"First name (optional)\">\n<\/p>\n<input type=\"hidden\" class=\"sib-SOURCE_DOMAIN-area\" name=\"SOURCE_DOMAIN\" value=\"yyc.ninja\">\n<input type=\"hidden\" class=\"sib-SIGNUP_SOURCE-area\" name=\"SIGNUP_SOURCE\" value=\"post\">\n<p>\n    <input type=\"submit\" class=\"sib-default-btn\" value=\"Join the Ninja Dispatch\">\n<\/p>\n<p class=\"ninja-fineprint\">No spam. Unsubscribe anytime.<\/p>\t\t\t\t<\/div>\n\t\t\t<\/form>\n\t\t\t<style>\n\t\t\t\tform#sib_signup_form_2{background:#192a3d;border-radius:10px;padding:16px;font-family:inherit}form#sib_signup_form_2 input[type=text],form#sib_signup_form_2 input[type=email],form#sib_signup_form_2 select{width:100%;box-sizing:border-box;margin:0 0 10px 0;padding:10px 12px;border:1px solid #33465c;border-radius:8px;background:#0f1c29;color:#e8eef5;font-size:14px}form#sib_signup_form_2 input::placeholder{color:#8a9bb0}form#sib_signup_form_2 .sib-default-btn{width:100%;margin:2px 0 0 0;padding:11px 14px;background:#e63946;color:#ffffff;border:0;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer}form#sib_signup_form_2 .sib-default-btn:hover{background:#d02f3c}form#sib_signup_form_2 p{margin:0 0 10px 0}form#sib_signup_form_2 .ninja-fineprint{margin:8px 0 0;font-size:11px;color:#8a9bb0;text-align:center}form#sib_signup_form_2 p.sib-alert-message {\n    padding: 6px 12px;\n    margin-bottom: 20px;\n    border: 1px solid transparent;\n    border-radius: 4px;\n    -webkit-box-sizing: border-box;\n    -moz-box-sizing: border-box;\n    box-sizing: border-box;\n}\nform#sib_signup_form_2 p.sib-alert-message-error {\n    background-color: #f2dede;\n    border-color: #ebccd1;\n    color: #a94442;\n}\nform#sib_signup_form_2 p.sib-alert-message-success {\n    background-color: #dff0d8;\n    border-color: #d6e9c6;\n    color: #3c763d;\n}\nform#sib_signup_form_2 p.sib-alert-message-warning {\n    background-color: #fcf8e3;\n    border-color: #faebcc;\n    color: #8a6d3b;\n}\n\t\t\t<\/style>\n\t\t\t\n","protected":false},"excerpt":{"rendered":"<p>Rolling upgrades assume spare capacity: drain a node, let its workloads reschedule, upgrade, repeat. This test platform was deliberately built without that margin, so it was a good place to find out what a safe upgrade looks like when the standard procedure cannot work.<\/p>\n","protected":false},"author":1,"featured_media":3593,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[12],"tags":[44,46,45],"class_list":["post-3594","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-how-to","tag-data-engineering","tag-mlops","tag-streaming"],"featured_image_urls_v2":{"full":["https:\/\/yyc.ninja\/wp-content\/uploads\/2026\/11\/cg-upgrading-kubernetes-rancher-cert-manager-no-spare-capacity-cover.png",1200,630,false],"thumbnail":["https:\/\/yyc.ninja\/wp-content\/uploads\/2026\/11\/cg-upgrading-kubernetes-rancher-cert-manager-no-spare-capacity-cover-150x150.png",150,150,true],"medium":["https:\/\/yyc.ninja\/wp-content\/uploads\/2026\/11\/cg-upgrading-kubernetes-rancher-cert-manager-no-spare-capacity-cover-300x158.png",300,158,true],"medium_large":["https:\/\/yyc.ninja\/wp-content\/uploads\/2026\/11\/cg-upgrading-kubernetes-rancher-cert-manager-no-spare-capacity-cover-768x403.png",768,403,true],"large":["https:\/\/yyc.ninja\/wp-content\/uploads\/2026\/11\/cg-upgrading-kubernetes-rancher-cert-manager-no-spare-capacity-cover-1024x538.png",1024,538,true],"RoboGalleryMansoryImagesCenter":["https:\/\/yyc.ninja\/wp-content\/uploads\/2026\/11\/cg-upgrading-kubernetes-rancher-cert-manager-no-spare-capacity-cover-600x630.png",600,630,true],"RoboGalleryPreload":["https:\/\/yyc.ninja\/wp-content\/uploads\/2026\/11\/cg-upgrading-kubernetes-rancher-cert-manager-no-spare-capacity-cover-100x53.png",100,53,true],"1536x1536":["https:\/\/yyc.ninja\/wp-content\/uploads\/2026\/11\/cg-upgrading-kubernetes-rancher-cert-manager-no-spare-capacity-cover.png",1200,630,false],"2048x2048":["https:\/\/yyc.ninja\/wp-content\/uploads\/2026\/11\/cg-upgrading-kubernetes-rancher-cert-manager-no-spare-capacity-cover.png",1200,630,false]},"post_excerpt_stackable_v2":"<p>Rolling upgrades assume spare capacity: drain a node, let its workloads reschedule, upgrade, repeat. This test platform was deliberately built without that margin, so it was a good place to find out what a safe upgrade looks like when the standard procedure cannot work.<\/p>\n","category_list_v2":"<a href=\"https:\/\/yyc.ninja\/index.php\/category\/how-to\/\" rel=\"category tag\">How To Ninja<\/a>","author_info_v2":{"name":"YYC Ninja","url":"https:\/\/yyc.ninja\/index.php\/author\/admin\/"},"comments_num_v2":"0 comments","_links":{"self":[{"href":"https:\/\/yyc.ninja\/index.php\/wp-json\/wp\/v2\/posts\/3594","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/yyc.ninja\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/yyc.ninja\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/yyc.ninja\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/yyc.ninja\/index.php\/wp-json\/wp\/v2\/comments?post=3594"}],"version-history":[{"count":1,"href":"https:\/\/yyc.ninja\/index.php\/wp-json\/wp\/v2\/posts\/3594\/revisions"}],"predecessor-version":[{"id":3595,"href":"https:\/\/yyc.ninja\/index.php\/wp-json\/wp\/v2\/posts\/3594\/revisions\/3595"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/yyc.ninja\/index.php\/wp-json\/wp\/v2\/media\/3593"}],"wp:attachment":[{"href":"https:\/\/yyc.ninja\/index.php\/wp-json\/wp\/v2\/media?parent=3594"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/yyc.ninja\/index.php\/wp-json\/wp\/v2\/categories?post=3594"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/yyc.ninja\/index.php\/wp-json\/wp\/v2\/tags?post=3594"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}