<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Kubeadm on ln --help</title>
    <link>https://blog.mei-home.net/tags/kubeadm/</link>
    <description>Recent content in Kubeadm on ln --help</description>
    <generator>Hugo -- 0.152.2</generator>
    <language>en</language>
    <lastBuildDate>Sun, 21 Sep 2025 23:30:40 +0200</lastBuildDate>
    <atom:link href="https://blog.mei-home.net/tags/kubeadm/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Updating my Kubeadm k8s Cluster from 1.30 to 1.33</title>
      <link>https://blog.mei-home.net/posts/kubernetes-cluster-update/</link>
      <pubDate>Sun, 21 Sep 2025 23:30:40 +0200</pubDate>
      <guid>https://blog.mei-home.net/posts/kubernetes-cluster-update/</guid>
      <description>Using Ansible to update my kubeadm k8s cluster from 1.30 to 1.33</description>
      <content:encoded><![CDATA[<p>Wherein I talk about updating my kubeadm Kubernetes cluster from 1.30 to 1.33
using Ansible.</p>
<p>I&rsquo;ve been a bit lax on my Kubernetes cluster updates, and I was still running
Kubernetes v1.30. I&rsquo;m also currently on a trip to fix a number of the smaller
tasks in my Homelab, paying down a bit of technical debt before tackling the
next big projects.</p>
<p>I already did one update, from my initial Kubernetes 1.29 to 1.30 in the past,
using an Ansible playbook I wrote to codify the kubeadm upgrade procedure. But
I never wrote a proper post about it, which I&rsquo;m now rectifying.</p>
<p>There were no really big problems - my cluster stayed up the entire time. But
there were issues in all three of the updates which might be of interest to
at least someone.</p>
<h2 id="the-kubeadm-cluster-update-procedure-and-version-skew">The kubeadm cluster update procedure and version skew</h2>
<p>The update of a kubeadm cluster is relatively straightforward, but it does
require some manual kubeadm actions directly on each node. The documentation
can be found <a href="https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade/">here</a>.</p>
<p>Please note: Those instructions are versioned, and may change in the future
compared to what I&rsquo;m describing here. Please make sure you&rsquo;re reading the
instructions pertinent to the version you&rsquo;re currently running.</p>
<p>The first thing to do is to read the release notes. These are very nicely prepared
by the Kubernetes team <a href="https://github.com/kubernetes/kubernetes/tree/master/CHANGELOG">here</a>,
sorted by major version.
And I approve of them wholeheartedly. I&rsquo;ve been known to rant a bit about release
engineering and release notes, but there&rsquo;s nothing to complain about when it comes
to Kubernetes. Besides perhaps their length, but that&rsquo;s to be expected in a
project of Kubernetes&rsquo; size.</p>
<p>I did not find anything relevant or interesting to me directly in any of the
releases, so I won&rsquo;t go into detail about the changes.</p>
<p>One thing to note, which will bite me later, is the <a href="https://kubernetes.io/releases/version-skew-policy/">version skew policy</a>.
It describes the allowed skew between versions, most importantly between the
kubelet and the kube-apiserver said kubelet is talking to. Namely, the versions
between the two can skew at most by a single minor version, and the kubelet must
not be newer than the kube-apiserver. Meaning the kube-apiserver always needs to
be updated first. More on this later, when I stumble over this policy.</p>
<p>Here is a short step-by-step of the kubeadm update process, always starting with
the control plane nodes:</p>
<ol>
<li>Update kubeadm to the new Kubernetes version</li>
<li>On the very first CP node, run <code>kubeadm upgrade apply v1.31.11</code>, for example</li>
<li>Then, update kubeadm on the other CP nodes and run <code>kubeadm upgrade node</code></li>
<li>Only after point 3) is completed on all nodes, update the kubelet as well</li>
</ol>
<p>The steps 2-4 are repeated for all non-CP nodes as well. The order of steps
3 and 4 is important. <code>kubeadm upgrade</code> needs to be run on all CP nodes before
any kubelet is updated. Or at least, that&rsquo;s true on a High Availability cluster,
where the kube-apiservers are sitting behind a virtual IP. That&rsquo;s because of
the version skew policy I mentioned above: The kubelet must never be newer than
the kube-apiserver it is talking to. Which makes some sense: The Kubernetes API
is the public API, with stability guarantees, backwards compatibility and such.
So it will likely be able to serve older kubelets just fine, as it will still
support the older APIs that kubelet depends on. But in the other direction, the
newer kubelet may access APIs which older kube-apiservers simply don&rsquo;t serve
yet.</p>
<h2 id="my-cluster-update-ansible-playbook">My cluster update Ansible playbook</h2>
<p>As I tend to do, I created an Ansible playbook during the first update, so that
I could do something else while the update runs fully automated. That did not work for any of the
updates this time around, but I will go into more detail later.</p>
<p>Let&rsquo;s start with the fact that I&rsquo;m using Ubuntu Linux as my OS on all of my
Homelab hosts, and I&rsquo;m getting the Kubernetes components from the official
apt repos provided by the Kubernetes project.
I&rsquo;m also using <a href="https://cri-o.io/">cri-o</a> as my container runtime. Until recently,
that was also hosted in the <a href="https://k8s.io">k8s.io</a> repos, but has since moved
to the <a href="https://www.opensuse.org/">openSUSE</a> repos.</p>
<p>Before starting the first tasks, here is my <code>group_vars/all.yml</code> file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">crio_version_prev</span>: <span style="color:#ae81ff">v1.30</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kube_version_prev</span>: <span style="color:#ae81ff">v1.30</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kube_version</span>: <span style="color:#ae81ff">v1.31</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kube_version_full</span>: <span style="color:#ae81ff">1.31.11</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">crio_version</span>: <span style="color:#ae81ff">v1.31</span>
</span></span></code></pre></div><p>I&rsquo;ve stored the versions here, instead of the <code>defaults/main.yml</code> of the role
because I also use the versions in a few other places, mainly my deployment
roles for configuring new cluster nodes.</p>
<p>But enough prelude, here are the first few tasks from the <code>tasks/main.yml</code> file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">update kubernetes repo key</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">copy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">src</span>: <span style="color:#ae81ff">kubernetes-keyring.gpg</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/usr/share/keyrings/kubernetes.gpg</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">owner</span>: <span style="color:#ae81ff">root</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">group</span>: <span style="color:#ae81ff">root</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mode</span>: <span style="color:#ae81ff">0644</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">remove old kubernetes deb repo</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">apt_repository</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">repo</span>: &gt;<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      deb [signed-by=/usr/share/keyrings/kubernetes.gpg]
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      https://pkgs.k8s.io/core:/stable:/{{ kube_version_prev }}/deb/ /</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">absent</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">filename</span>: <span style="color:#ae81ff">kubernetes</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">ansible_facts[&#39;distribution&#39;] == &#39;Ubuntu&#39;</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">add kubernetes ubuntu repo</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">apt_repository</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">repo</span>: &gt;<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      deb [signed-by=/usr/share/keyrings/kubernetes.gpg]
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      https://pkgs.k8s.io/core:/stable:/{{ kube_version }}/deb/ /</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">present</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">filename</span>: <span style="color:#ae81ff">kubernetes</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">ansible_facts[&#39;distribution&#39;] == &#39;Ubuntu&#39;</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">update apt after kubernetes repos changed</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">apt</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">update_cache</span>: <span style="color:#66d9ef">yes</span>
</span></span></code></pre></div><p>These deploy the apt key of the <code>K8s.io</code> repo for the main Kubernetes components,
remove the repo of the previous version and add the repo of the new version.
Finally, an apt cache update is executed to fetch the packages from the new repo
before running any install tasks.</p>
<p>One thing to note here is that I&rsquo;m manually fetching the Kubernetes repo key
and storing it in the repo via this command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | gpg --dearmor -o roles/kube-common/files/kubernetes-keyring.gpg
</span></span></code></pre></div><p>The next step is updating the kubeadm version:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">unpin kubeadm version</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">dpkg_selections</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">kubeadm</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">selection</span>: <span style="color:#ae81ff">install</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">update_kubeadm</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">update kubeadm</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ansible.builtin.apt</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;kubeadm={{ kube_version_full }}*&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">present</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">install_recommends</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">update_kubeadm</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">pin kubeadm version</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">dpkg_selections</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">kubeadm</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">selection</span>: <span style="color:#ae81ff">hold</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">update_kubeadm</span>
</span></span></code></pre></div><p>The <code>update_kubeadm</code> variable is necessary because I&rsquo;m running this role twice for control plane nodes.
Once updating only kubeadm on all CP nodes, and then again to run the kubelet
update. But that second run won&rsquo;t need to run the kubeadm update again, hence
why the <code>update_kubeadm</code> variable exists.</p>
<p>Next is the <code>kubeadm upgrade</code> invocation, the main part of the cluster update:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">run kubeadm update</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">command</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cmd</span>: <span style="color:#e6db74">&#34;kubeadm upgrade node&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">not kube_first_node and update_kubeadm</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">run kubeadm update</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">command</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cmd</span>: <span style="color:#e6db74">&#34;kubeadm upgrade apply -y v{{ kube_version_full }}&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">kube_first_node and update_kubeadm</span>
</span></span></code></pre></div><p>There are two variants of this task, depending on whether <code>kube_first_node</code> is
set or not. This is necessary because only the first CP node updated needs to
run <code>upgrade apply -y v&lt;NEW_VERSION&gt;</code>. All other CP nodes and all non-CP nodes
just run <code>upgrade node</code>. Again, this setup using variables is mostly because
<em>in principle</em>, the update steps are the same for all nodes in the cluster. So
it made more sense to have one role where I could switch some tasks on/off, rather
than having multiple roles which each repeat a lot of their respective tasks.
The kubeadm update includes updating the control plane components: kube-apiserver,
kube-controller-manager and kube-scheduler as well as etcd. All of these are
static Pods, who&rsquo;s definition is controlled by kubeadm.</p>
<p>The next step is updating the kubelet and kubectl on the nodes, which is
headed by draining the node:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">drain node</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">kubernetes</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">ceph</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">delegate_to</span>: <span style="color:#ae81ff">candc</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become_user</span>: <span style="color:#ae81ff">myuser</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">command</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">argv</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">kubectl</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">drain</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">delete-emptydir-data=true</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">force=true</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">ignore-daemonsets=true</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;{{ ansible_hostname }}&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">update_non_kubeadm</span>
</span></span></code></pre></div><p>Here is the second variable I&rsquo;m using to restrict which tasks of the role are
executed for a particular host, the <code>update_non_kubeadm</code> variable. It indicates
that all tasks not related to the kubeadm update are to be executed.
This command is not issued on the node itself, but rather on my command and
control host, which also runs the Ansible playbook.</p>
<p>Then comes the update of cri-o:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">remove previous kube cri-o repo</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">apt_repository</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">repo</span>: &gt;<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      deb [signed-by=/usr/share/keyrings/libcontainers-crio-keyring.gpg]
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      https://download.opensuse.org/repositories/isv:/cri-o:/stable:/{{ crio_version_prev }}/deb/ /</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">absent</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">filename</span>: <span style="color:#ae81ff">libcontainers-crio</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">ansible_facts[&#39;distribution&#39;] == &#39;Ubuntu&#39; and update_non_kubeadm</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">add libcontainers cri-o repo key</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">copy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">src</span>: <span style="color:#ae81ff">libcontainers-crio-keyring.gpg</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/usr/share/keyrings/libcontainers-crio-keyring.gpg</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">owner</span>: <span style="color:#ae81ff">root</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">group</span>: <span style="color:#ae81ff">root</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mode</span>: <span style="color:#ae81ff">0644</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">update_non_kubeadm</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">add kube cri-o repo</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">apt_repository</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">repo</span>: &gt;<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      deb [signed-by=/usr/share/keyrings/libcontainers-crio-keyring.gpg]
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      https://download.opensuse.org/repositories/isv:/cri-o:/stable:/{{ crio_version }}/deb/ /</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">present</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">filename</span>: <span style="color:#ae81ff">libcontainers-crio</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">ansible_facts[&#39;distribution&#39;] == &#39;Ubuntu&#39; and update_non_kubeadm</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">update apt after cri-o repos changed</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">apt</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">update_cache</span>: <span style="color:#66d9ef">yes</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">update_non_kubeadm</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">update cri-o</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ansible.builtin.apt</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">cri-o</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">cri-tools</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">install_recommends</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">update_non_kubeadm</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">autostart cri-o</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ansible.builtin.systemd_service</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">crio</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">started</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">update_non_kubeadm</span>
</span></span></code></pre></div><p>This is similar to the initial Kubernetes repo setup. Please note that from
version 1.30 to 1.32, cri-o lived in the k8s.io repos, but was then moved to
openSUSE repos.</p>
<p>Once cri-o is updated, the last part of the role is updating kubectl and kubelet:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">unpin kubelet version</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">dpkg_selections</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">kubelet</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">selection</span>: <span style="color:#ae81ff">install</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">update_non_kubeadm</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">update kubelet</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ansible.builtin.apt</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;kubelet={{ kube_version_full }}*&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">present</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">install_recommends</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">update_non_kubeadm</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">pin kubelet version</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">dpkg_selections</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">kubelet</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">selection</span>: <span style="color:#ae81ff">hold</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">update_non_kubeadm</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">unpin kubectl version</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">dpkg_selections</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">kubectl</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">selection</span>: <span style="color:#ae81ff">install</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">update_non_kubeadm</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">update kubectl</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ansible.builtin.apt</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;kubectl={{ kube_version_full }}*&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">present</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">install_recommends</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">update_non_kubeadm</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">pin kubectl version</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">dpkg_selections</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">kubectl</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">selection</span>: <span style="color:#ae81ff">hold</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">update_non_kubeadm</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">restart kubelet</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">systemd_service</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">kubelet</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">daemon_reload</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">restarted</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">update_non_kubeadm</span>
</span></span></code></pre></div><p>And finally, the node is uncordoned:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">uncordon node</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">delegate_to</span>: <span style="color:#ae81ff">candc</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become_user</span>: <span style="color:#ae81ff">myuser</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">kubernetes.core.k8s_drain</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;{{ ansible_hostname }}&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">uncordon</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">update_non_kubeadm</span>
</span></span></code></pre></div><p>This command is again delegated to my command and control host, which means the
command is not executed on the remote host by my Ansible user, but rather for
every host, the kubectl command is executed on a central host which has the
necessary permissions and keys to actually run kubectl against the cluster.</p>
<p>The role I&rsquo;ve described above is then used in a playbook running it against
the different groups of hosts in my Homelab. First is one of the control plane
hosts, running the required first <code>kubeadm upgrade apply -y &lt;NEW_KUBE_VERSION&gt;</code>
command, which only needs to be run on the first control plane node:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">hosts</span>: <span style="color:#ae81ff">firstcp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Update first kubernetes controller kubeadm</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">k8s-update-kubeadm-first</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">serial</span>: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">strategy</span>: <span style="color:#ae81ff">linear</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tasks</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">include cluster upgrade role</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">include_role</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">name</span>: <span style="color:#ae81ff">kube-cluster-upgrade</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">vars</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">kube_first_node</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">update_kubeadm</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">update_non_kubeadm</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">pause for two minutes</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#ae81ff">kubernetes</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">pause</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">minutes</span>: <span style="color:#ae81ff">2</span>
</span></span></code></pre></div><p>Notably, this run gets the <code>kube_first_node</code> variable set, but doesn&rsquo;t run the
non-kubeadm updates, meaning the kubelet update, yet.
Next come the remaining control plane nodes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">hosts</span>: <span style="color:#ae81ff">kube_controllers:!firstcp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Update other kubernetes controllers kubeadm</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">k8s-update-kubeadm</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">serial</span>: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">strategy</span>: <span style="color:#ae81ff">linear</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tasks</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">include cluster upgrade role</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">include_role</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">name</span>: <span style="color:#ae81ff">kube-cluster-upgrade</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">vars</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">update_kubeadm</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">update_non_kubeadm</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">pause for two minutes</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#ae81ff">kubernetes</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">pause</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">minutes</span>: <span style="color:#ae81ff">2</span>
</span></span></code></pre></div><p>These nodes don&rsquo;t have the <code>kube_first_node</code> set, so they execute the <code>kubeadm upgrade node</code>
update command. Here, too, <code>update_non_kubeadm</code> is false, meaning the kubelets
are not updated yet. This is necessary because without this, there&rsquo;s a danger
that a kubelet that has already been updated would talk to a kube-apiserver which
hasn&rsquo;t yet been updated, potentially leading to errors.</p>
<p>After the kubeadm update follows the kubelet update for the controller nodes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">hosts</span>: <span style="color:#ae81ff">kube_controllers</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Update kubernetes controllers</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">k8s-update-controllers</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">serial</span>: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">strategy</span>: <span style="color:#ae81ff">linear</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tasks</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">include cluster upgrade role</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">include_role</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">name</span>: <span style="color:#ae81ff">kube-cluster-upgrade</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">vars</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">update_kubeadm</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">update_non_kubeadm</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">wait for vault to be running</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#ae81ff">kubernetes</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">delegate_to</span>: <span style="color:#ae81ff">candc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">become_user</span>: <span style="color:#ae81ff">myuser</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">kubernetes.core.k8s_info</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Pod</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">vault</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">label_selectors</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#ae81ff">app.kubernetes.io/name=vault</span>
</span></span><span style="display:flex;"><span>          - <span style="color:#ae81ff">app.kubernetes.io/instance=vault</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">field_selectors</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#e6db74">&#34;spec.nodeName={{ ansible_hostname }}&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">wait</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">wait_condition</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">status</span>: <span style="color:#e6db74">&#34;True&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">type</span>: <span style="color:#e6db74">&#34;Ready&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">wait_sleep</span>: <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">wait_timeout</span>: <span style="color:#ae81ff">300</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">register</span>: <span style="color:#ae81ff">vault_pod_list</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">unseal vault prompt</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#ae81ff">vault</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">pause</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">echo</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">prompt</span>: <span style="color:#e6db74">&#34;Please unseal vault: k exec -it -n vault {{ vault_pod_list.resources[0].metadata.name }} -- vault operator unseal&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">pause for two minutes</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#ae81ff">kubernetes</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">pause</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">minutes</span>: <span style="color:#ae81ff">2</span>
</span></span></code></pre></div><p>This runs the role with <code>update_kubeadm: false</code> but <code>update_non_kubeadm: true</code>,
leading to the kubeadm update being skipped as it was already run in the previous
play, and instead the kubelet is being updated. This is safe to do now, because
all kube-apiservers have been updated to the new version at this point.
I&rsquo;m running a two minute pause task at the end of each play, to give the cluster
a bit of time to start all Pods again.
This kubelet update step also contains some handling of my Vault containers, which
are running on the control plane nodes. They need to be manually unsealed
when they&rsquo;re restarted.</p>
<p>Next up are the Ceph nodes, which I do not throw together with the rest of the
worker nodes as they need to be run one at a time, to prevent storage downtime.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">hosts</span>: <span style="color:#ae81ff">kube_ceph</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Update kubernetes Ceph nodes</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">k8s-update-ceph</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">serial</span>: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">strategy</span>: <span style="color:#ae81ff">linear</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pre_tasks</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">set osd noout</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">delegate_to</span>: <span style="color:#ae81ff">candc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">become_user</span>: <span style="color:#ae81ff">myuser</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">command</span>: <span style="color:#ae81ff">/home/myuser/.krew/bin/kubectl-rook_ceph --operator-namespace rook-ceph -n rook-cluster ceph osd set noout</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tasks</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">include cluster upgrade role</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">include_role</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">name</span>: <span style="color:#ae81ff">kube-cluster-upgrade</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">vars</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">update_kubeadm</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">update_non_kubeadm</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">wait for OSDs to start</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">delegate_to</span>: <span style="color:#ae81ff">candc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">become_user</span>: <span style="color:#ae81ff">myuser</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#ae81ff">ceph</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">command</span>: <span style="color:#ae81ff">/home/myuser/.krew/bin/kubectl-rook_ceph --operator-namespace rook-ceph -n rook-cluster ceph osd status &#34;{{ ansible_hostname }}&#34; --format json</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">register</span>: <span style="color:#ae81ff">ceph_end</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">until</span>: <span style="color:#e6db74">&#34;(ceph_end.stdout | trim | from_json | community.general.json_query(&#39;OSDs[*].state&#39;) | select(&#39;contains&#39;, &#39;up&#39;) | length) == (ceph_end.stdout | trim | from_json | community.general.json_query(&#39;OSDs[*]&#39;) | length)&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">retries</span>: <span style="color:#ae81ff">12</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">delay</span>: <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">pause for two minutes</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#ae81ff">ceph</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">pause</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">minutes</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">post_tasks</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">unset osd noout</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">delegate_to</span>: <span style="color:#ae81ff">candc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">become_user</span>: <span style="color:#ae81ff">myuser</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">command</span>: <span style="color:#ae81ff">/home/myuser/.krew/bin/kubectl-rook_ceph --operator-namespace rook-ceph -n rook-cluster ceph osd unset noout</span>
</span></span></code></pre></div><p>I&rsquo;m also setting the <code>noout</code> flag for Ceph. This ensures that Ceph doesn&rsquo;t start
automatic rebalancing when the OSDs on the upgraded host temporarily go down.
In addition, I&rsquo;m waiting for the OSDs on each host to be up again before continuing
to the next host, to prevent storage issues.</p>
<p>Last but not least are my worker nodes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">hosts</span>: <span style="color:#ae81ff">kube_workers</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Update kubernetes worker nodes</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">k8s-update-workers</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">serial</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">strategy</span>: <span style="color:#ae81ff">linear</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pre_tasks</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tasks</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">include cluster upgrade role</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">include_role</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">name</span>: <span style="color:#ae81ff">kube-cluster-upgrade</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">vars</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">update_kubeadm</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">update_non_kubeadm</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">pause for one minute</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#ae81ff">kubernetes</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">pause</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">minutes</span>: <span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p>Nothing special about these. In contrast to all the other plays, I&rsquo;m running
two hosts in parallel through it, because I do currently have enough slack in
the cluster to be able to tolerate the loss of two workers.</p>
<p>So now let me tell you how that beautiful theory I laid out up to now actually
worked in practice. &#x1f601;</p>
<h2 id="a-tale-of-three-updates">A tale of three updates</h2>
<p>I upgraded from Kubernetes 1.30 all the way to 1.33. None of the three went
through without at least one issue.</p>
<h3 id="updating-from-130-to-131">Updating from 1.30 to 1.31</h3>
<p>This one was the most complicated when it came to fixing the issue. I started
it with the previous iteration of my update playbook, which still fully updated
each control plane node in turn. So it first ran the kubeadm update on one
node and then immediately followed that up with updating the kubelet on that same
node.
Right on the first node, I was greeted with these errors for a number of the
Pods:</p>
<pre tabindex="0"><code>NAMESPACE     NAME                                             READY   STATUS                            RESTARTS      AGE
fluentbit     fluentbit-fluent-bit-km8r7                       0/1     CreateContainerConfigError        0             38m
kube-system   cilium-98hzq                                     0/1     Init:CreateContainerConfigError   0             14m
kube-system   cilium-envoy-tklh7                               0/1     CreateContainerConfigError        0             40m
kube-system   etcd-firstcp                                     1/1     Running                           2 (35m ago)   35m
kube-system   kube-apiserver-firstcp                           1/1     Running                           2 (35m ago)   35m
kube-system   kube-controller-manager-firstcp                  1/1     Running                           0             35m
kube-system   kube-scheduler-firstcp                           1/1     Running                           0             35m
kube-system   kube-vip-firstcp                                 1/1     Running                           0             35m
rook-ceph     rook-ceph.cephfs.csi.ceph.com-nodeplugin-bnmsd   0/3     CreateContainerConfigError        0             38m
rook-ceph     rook-ceph.rbd.csi.ceph.com-nodeplugin-hq82g      0/3     CreateContainerConfigError        0             38m
</code></pre><p>Note the error in the <code>STATUS</code> of all of the non-kube Pods. I had never heard
of a <code>CreateContainerConfigError</code> before, so I went to google and found
<a href="https://github.com/kubernetes/kubernetes/issues/127316">this issue</a>. It identified
the problem pretty clearly and the kubernetes maintainers helpfully pointed
to the <a href="https://kubernetes.io/releases/version-skew-policy/#kubelet">version-skew-policy</a>.
After reading said policy multiple times, I finally realized what my error was and
updated my Ansible playbook to first update all kubeadm versions on all CP nodes
and only then start updating the kubelet. I got the error fixed by just running
the kubeadm update on the other two control plane nodes as well.</p>
<p>After that, the rest of the update went through without a hitch.</p>
<h3 id="updating-from-131-to-132">Updating from 1.31 to 1.32</h3>
<p>In this one I stumbled over the fact that I hadn&rsquo;t fully understood the
release notes for 1.32, or rather their implications. Specifically, this point
in the <a href="https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.32.md#v1320">1.32 release notes</a>:</p>
<blockquote>
<p>kubeadm: kubeadm upgrade node now supports addon and post-upgrade phases. Users can use kubeadm upgrade node phase addon to execute the addon upgrade, or use kubeadm upgrade node &ndash;skip-phases addon to skip the addon upgrade. If you were previously skipping an addon subphase on kubeadm init you should now skip the same addon when calling kubeadm upgrade apply and kubeadm upgrade node. Currently, the post-upgrade phase is no-op, and it is mainly used to handle some release-specific post-upgrade tasks.</p>
</blockquote>
<p>So basically, addons, like kube-proxy for example, had been ignored during updates
up to this point. Which is why my updates worked up to now. But in 1.32,
the <code>kubeadm upgrade</code> command gained the ability to also update addons. And
seemingly also deploy them if they&rsquo;re not present, because I suddenly found
kube-proxy Pods on my nodes after the upgrade.</p>
<p>I did not use kube-proxy, because I was using Cilium&rsquo;s kube-proxy replacement.
I had disabled kube-proxy in my <code>InitConfiguration</code> like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">skipPhases</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#e6db74">&#34;addon/kube-proxy&#34;</span>
</span></span></code></pre></div><p>But, the InitConfiguration isn&rsquo;t read during updates, and it seems that kubeadm
doesn&rsquo;t transfer this setting into the <code>kubeadm-config</code> ConfigMap during cluster
creation. So <code>kubeadm upgrade</code> didn&rsquo;t have any idea that it should be skipping
the addon, and happily deployed it on my nodes.</p>
<p>Luckily for me, it didn&rsquo;t seem to interfere with anything, and my cluster didn&rsquo;t
just collapse in on itself. I removed them all with the handy instructions from
the <a href="https://docs.cilium.io/en/stable/network/kubernetes/kubeproxy-free/#quick-start">Cilium docs</a>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>kubectl -n kube-system delete ds kube-proxy
</span></span><span style="display:flex;"><span>kubectl -n kube-system delete cm kube-proxy
</span></span></code></pre></div><p>To prevent any further issues, I edited the kubeadm-config file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>kubectl get -n kube-system configmaps kubeadm-config -o yaml
</span></span></code></pre></div><p>And added an entry <code>proxy.disabled: true</code> to it. With this, the problem did not
occur again during the subsequent 1.33 update.</p>
<h3 id="updating-from-132-to-133">Updating from 1.32 to 1.33</h3>
<p>The last one. I was hoping it would go through without an issue, to at least
have one successful update during which I could move away from the computer and
read a bit, but no such luck.</p>
<p>During the update of the cri-o repository for 1.33, I got this error:</p>
<pre tabindex="0"><code>Failed to update apt cache: E:Failed to fetch https://pkgs.k8s.io/addons:/cri-o:/stable:/v1.33/deb/InRelease  403  Forbidden [IP: 3.167.227.100 443]
</code></pre><p>This was because cri-o&rsquo;s repos moved from k8s.io to openSUSE, see for example
<a href="https://github.com/cri-o/cri-o/issues/9341">this issue</a>. The adaption was
pretty simple, I just needed to change the address in my playbook.</p>
<p>After that fix, the update ran through without any further issues and I was
finally done. Cost me almost a day of work, but alas, most of the issues were of
my own making.</p>
<h2 id="increased-memory-requests">Increased memory requests?</h2>
<p>And finally for something amusing. When I looked at my Homelab dashboard on
the morning after the upgrade, I found that the memory requests for my worker
nodes were suddenly in the red, with almost 83% of available capacity used:</p>
<p><figure>
    <img loading="lazy" src="resource-usage.png"
         alt="A screenshot of several Grafana gauge visualizations. They show the utilization of memory and CPU resource usage in my k8s cluster, as measured by looking at the total resource requests from all Pods in the cluster. There are three gauges, one for each of my node groups, &#39;Control Plane&#39;, &#39;Ceph&#39; and &#39;Workers&#39;. Interesting here are the values for the &#39;Workers&#39; group, which show 72.5% for the CPU resource consumption and 82.8% for the memory resource consumption."/> <figcaption>
            <p>Resource usage the morning after the update. This shows the sum of resource requests on Pods divided by the overall resources of the group of nodes.</p>
        </figcaption>
</figure>

Normally, the memory utilization is more around 60%.</p>
<p>Thinking that the update must have changed something in how the memory utilization
was computed, or perhaps there was some Deployment which increased memory requests
after the update, I looked through my metrics, but wasn&rsquo;t able to find anything.</p>
<p>After some additional checking, I finally found the issue in how I was computing
the values for the metric:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-promql" data-lang="promql"><span style="display:flex;"><span><span style="color:#f92672">(</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">sum</span><span style="color:#f92672">(</span>
</span></span><span style="display:flex;"><span>      kube_pod_container_resource_requests{resource<span style="color:#f92672">=</span>&#34;<span style="color:#e6db74">memory</span>&#34;}
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">and</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">on</span><span style="color:#f92672">(</span>pod<span style="color:#f92672">)</span> <span style="color:#f92672">(</span>kube_pod_status_phase{phase<span style="color:#f92672">=</span>&#34;<span style="color:#e6db74">Running</span>&#34;} <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span><span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">unless</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">on</span><span style="color:#f92672">(</span>node<span style="color:#f92672">)</span> <span style="color:#f92672">(</span>kube_node_spec_taint{}<span style="color:#f92672">))</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">/</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">sum</span><span style="color:#f92672">(</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">(</span>
</span></span><span style="display:flex;"><span>      kube_node_status_capacity{resource<span style="color:#f92672">=</span>&#34;<span style="color:#e6db74">memory</span>&#34;}
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">unless</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">on</span><span style="color:#f92672">(</span>node<span style="color:#f92672">)</span> kube_node_spec_taint{}
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">)</span>
</span></span></code></pre></div><p>So I&rsquo;m using the <code>kube_pod_container_resource_requests</code> for the <code>memory</code> resource,
but only for Pods on nodes where there is no taint. Then I divide that by the
memory capacity of all nodes which don&rsquo;t have a taint. I use this because the taint
was readily available in the Prometheus data, and my worker nodes are the only
ones which don&rsquo;t have a taint applied to them, so it made sense to use them.</p>
<p>What I did not consider: There are a few non-catastrophic taints which Kubernetes
applies, in my case the disk pressure taint. This simply happened because the disks
were getting a bit full on a few worker nodes due to the many node drains and
subsequent reschedules of Pods. So there were a lot more unused images laying
around locally than was normally the case.</p>
<p>I was quite amused with myself when I realized that I had just spend half an
hour staring at completely the wrong plots. &#x1f601;</p>
<p>And that&rsquo;s it. Here&rsquo;s to hoping that the next Kubernetes update is not interesting
enough to blog about.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
