<?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>Kubernetes on vishctl</title><link>https://vishctl.dev/tags/kubernetes/</link><description>Recent content in Kubernetes on vishctl</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Tue, 08 Sep 2026 13:30:00 +0800</lastBuildDate><atom:link href="https://vishctl.dev/tags/kubernetes/index.xml" rel="self" type="application/rss+xml"/><item><title>Run vLLM on Kubernetes with Minikube, WSL2 and NVIDIA GPU</title><link>https://vishctl.dev/posts/vllm-on-wsl2-minikube/</link><pubDate>Tue, 08 Sep 2026 13:30:00 +0800</pubDate><guid>https://vishctl.dev/posts/vllm-on-wsl2-minikube/</guid><category>ai</category><category>local-llm</category><category>vllm</category><category>kubernetes</category><category>minikube</category><category>wsl2</category><category>gpu</category><category>nvidia</category><description>A practical guide to running vLLM on Kubernetes in WSL2 with Minikube, configuring NVIDIA GPU passthrough, and serving an OpenAI-compatible API.</description><content:encoded><![CDATA[<p>This is the <a href="https://docs.vllm.ai/">vLLM</a> entry in my local AI series. After testing <a href="https://vishctl.dev/posts/running-ollama-on-32gb-macbook-air/">Ollama</a>, <a href="https://vishctl.dev/posts/running-llama-cpp-on-32gb-macbook-air/">llama.cpp</a>, and <a href="https://vishctl.dev/posts/running-freetoken-on-8gb-laptop-gpu/">FreeToken</a>, I wanted to run vLLM as a Kubernetes Deployment on my Windows/WSL2 setup.</p>
<p>The plan was simple: deploy vLLM, request <code>nvidia.com/gpu: 1</code>, expose an OpenAI-compatible API endpoint through a Service, and tie it into the Kubernetes workflows I write about regularly.</p>
<p>Getting a GPU into Kubernetes on WSL2 turned into an investigation. Not because vLLM is hard, but because container runtimes and nested clusters handle GPU passthrough in non-obvious ways. Here is what happened, how the device plugin and node prerequisites work, and how to get a working GPU cluster running with Minikube.</p>
<hr>
<h2 id="what-youll-build">What You&rsquo;ll Build</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Windows
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ▼
</span></span><span class="line"><span class="cl">WSL2 Ubuntu
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ▼
</span></span><span class="line"><span class="cl">Docker Engine + NVIDIA Container Toolkit
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ▼
</span></span><span class="line"><span class="cl">Minikube
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ▼
</span></span><span class="line"><span class="cl">Kubernetes
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ├── NVIDIA GPU Operator
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ├── NVIDIA Device Plugin
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   └── vLLM
</span></span><span class="line"><span class="cl">         │
</span></span><span class="line"><span class="cl">         ▼
</span></span><span class="line"><span class="cl">      NVIDIA GPU
</span></span></code></pre></div><p>By the end of this guide, you&rsquo;ll have vLLM running on Kubernetes with GPU acceleration and serving an OpenAI-compatible API.</p>
<hr>
<h2 id="the-test-rig">The Test Rig</h2>
<p>My test machine for this run:</p>
<ul>
<li><strong>Host Machine:</strong> Windows 11 Laptop</li>
<li><strong>GPU:</strong> NVIDIA GeForce RTX 4070 Laptop GPU (8.0 GiB VRAM)</li>
<li><strong>Host Memory:</strong> 32 GB DDR5</li>
<li><strong>WSL2 Environment:</strong> Ubuntu 24.04 LTS (noble) with systemd enabled</li>
<li><strong>NVIDIA Drivers:</strong> Driver version 616.56 / CUDA 13.4 user-mode driver</li>
<li><strong>Kubernetes:</strong> Minikube v1.39.0 provisioning Kubernetes v1.37.0</li>
<li><strong>Model:</strong> Qwen3.5-0.8B (<code>Qwen/Qwen3.5-0.8B</code>) served by vLLM v0.28.0</li>
</ul>
<hr>
<h2 id="troubleshooting-vllm-gpu-support-on-docker-desktop-wsl2-kubernetes">Troubleshooting vLLM GPU Support on Docker Desktop WSL2 Kubernetes</h2>
<p>Docker Desktop offers a one-click Kubernetes cluster in its settings. Turning it on provisions a single-node cluster named <code>desktop-control-plane</code> running on <code>containerd://2.3.4</code>.</p>
<p>On the WSL2 host, <code>nvidia-smi</code> works fine, and running standalone GPU containers (<code>docker run --gpus all</code>) works without issues. The next step was applying the standard NVIDIA Kubernetes device plugin DaemonSet to enable GPU scheduling:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.16.2/deployments/static/nvidia-device-plugin.yml
</span></span></code></pre></div><p>The DaemonSet pod started, but immediately stalled in a retry loop:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">E... Incompatible strategy detected auto
</span></span><span class="line"><span class="cl">E... If this is a GPU node, did you configure the NVIDIA Container Toolkit?
</span></span><span class="line"><span class="cl">I... No devices found. Waiting indefinitely.
</span></span></code></pre></div><p>Checking the node capacity confirmed the issue:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl describe node desktop-control-plane <span class="p">|</span> grep -A5 <span class="s2">&#34;Capacity:\|Allocatable:&#34;</span>
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/01-docker-desktop-k8s-no-gpu.png"
         alt="Docker Desktop Kubernetes node missing GPU resources"/> <figcaption>
            <p>Checking Docker Desktop&rsquo;s desktop-control-plane: nvidia-smi finds the RTX 4070, but the node exposes zero GPU capacity.</p>
        </figcaption>
</figure>

<p>In this tested Docker Desktop Kubernetes setup, GPU resources were not exposed to the Kubernetes node. Despite GPU access working in WSL2 and standalone Docker containers, the Kubernetes node could not expose <code>nvidia.com/gpu</code> to the device plugin.</p>
<hr>
<h2 id="how-kubernetes-detects-nvidia-gpus-using-the-nvidia-device-plugin">How Kubernetes Detects NVIDIA GPUs Using the NVIDIA Device Plugin</h2>
<p>The <a href="https://github.com/NVIDIA/k8s-device-plugin">NVIDIA Kubernetes Device Plugin</a> is a DaemonSet that exposes GPU hardware to the Kubernetes control plane. It does not run inference, and it does not install drivers or container runtimes.</p>
<p>Under standard <a href="https://kubernetes.io/docs/tasks/manage-gpus/scheduling-gpus/">Kubernetes GPU scheduling</a>, pods request GPU resources by specifying limits for extended resources. The device plugin integrates with Kubelet through the Kubernetes Device Plugin API over gRPC, using a Unix domain socket at <code>/var/lib/kubelet/device-plugins/kubelet.sock</code>. Its responsibility comes down to three functions:</p>
<ol>
<li><strong>Discovery:</strong> The plugin queries the host system using NVML (NVIDIA Management Library) to check how many physical GPUs are present and verify their health status.</li>
<li><strong>Registration:</strong> It registers with Kubelet and advertises the discovered GPUs as an extended allocatable resource named <code>nvidia.com/gpu</code>.</li>
<li><strong>Allocation:</strong> When a pod requesting <code>nvidia.com/gpu: 1</code> gets scheduled to the node, Kubelet calls the plugin&rsquo;s <code>Allocate</code> gRPC endpoint. The plugin picks a healthy GPU and sends back the device IDs and environment variables (<code>NVIDIA_VISIBLE_DEVICES=&lt;UUID&gt;</code>) to Kubelet. Kubelet then passes those variables to the container runtime so the container gets access to <code>/dev/nvidia*</code>.</li>
</ol>
<h3 id="worker-node-prerequisites">Worker Node Prerequisites</h3>
<p>Because the device plugin only handles discovery and Kubelet registration, it assumes the worker node already has a functional GPU stack. For worker nodes in any Kubernetes cluster, three layers must be in place before the device plugin can run:</p>
<ol>
<li><strong>Host Kernel Drivers:</strong> The node OS must have the NVIDIA kernel modules loaded (<code>nvidia.ko</code>, <code>nvidia-uvm.ko</code>) and device nodes created in <code>/dev</code>. Running <code>nvidia-smi</code> on the host must return clean output.</li>
<li><strong>NVIDIA Container Toolkit:</strong> Packages such as <code>libnvidia-container</code> and <code>nvidia-container-toolkit</code> must be installed on the host. Refer to the <a href="https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html">NVIDIA Container Toolkit install guide</a> for distribution setup. This toolkit includes the OCI prestart hook that inspects container environment variables and mounts host GPU driver libraries into the target container.</li>
<li><strong>Runtime Configuration:</strong> The node container runtime (containerd or Docker) must have the NVIDIA runtime configured in its daemon settings (such as <code>/etc/containerd/config.toml</code> or <code>/etc/docker/daemon.json</code>) and set up to handle CDI (Container Device Interface) or NVIDIA runtime hooks.</li>
</ol>
<p>If any of those three pieces is missing or unconfigured, the device plugin pod will fail during startup or report zero allocatable GPUs.</p>
<hr>
<h2 id="why-docker-desktop-kubernetes-cannot-detect-my-nvidia-gpu">Why Docker Desktop Kubernetes Cannot Detect My NVIDIA GPU</h2>
<p>Based on the runtime architecture observed in this setup, the issue comes down to how Docker Desktop isolates its cluster node.</p>
<p><code>desktop-control-plane</code> runs inside Docker Desktop&rsquo;s private utility VM (<code>docker-desktop</code>), isolated using <strong><code>sysbox-runc</code></strong> rather than the standard OCI runtime <code>runc</code>. Sysbox provides nested container virtualization to spin up systemd, kubelet, and containerd within an unprivileged container environment.</p>
<p>In this setup, that isolation layer prevents GPU passthrough:</p>
<ul>
<li><strong>Missing OCI Passthrough:</strong> The node container was not launched with NVIDIA GPU passthrough flags (<code>--gpus all</code>). Inside the sandbox, containerd cannot access NVIDIA device nodes (<code>/dev/nvidia*</code>) or the WSL2 DirectX driver mapping (<code>/usr/lib/wsl/lib</code>).</li>
<li><strong>Locked-Down Lifecycle:</strong> You cannot configure the NVIDIA Container Toolkit inside the node&rsquo;s containerd because Docker Desktop internally supervises the container.</li>
<li><strong>Daemon Defaults Do Not Propagate:</strong> Setting <code>nvidia</code> as the default OCI runtime in <code>docker-desktop</code> WSL does not alter the <code>sysbox-runc</code> runtime used by the Kubernetes node container.</li>
</ul>
<p>Docker has an open roadmap issue requesting native GPU passthrough for Docker Desktop Kubernetes. It remains an architectural boundary in the current setup.</p>
<p>Because of these issues, I pivoted to running Minikube with WSL2 using native Docker.</p>
<hr>
<h2 id="installing-docker-ce-and-containerd-inside-wsl2">Installing Docker CE and Containerd Inside WSL2</h2>
<p>To satisfy those node prerequisites and bypass Docker Desktop&rsquo;s VM isolation, we install the native Docker Community Edition engine directly inside the Ubuntu 24.04 WSL2 environment:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Add Docker&#39;s official GPG key</span>
</span></span><span class="line"><span class="cl">sudo mkdir -p /etc/apt/keyrings
</span></span><span class="line"><span class="cl">sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
</span></span><span class="line"><span class="cl">sudo chmod a+r /etc/apt/keyrings/docker.asc
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Set up the repository</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;deb [arch=</span><span class="k">$(</span>dpkg --print-architecture<span class="k">)</span><span class="s2"> signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable&#34;</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Install Docker CE and containerd</span>
</span></span><span class="line"><span class="cl">sudo apt update
</span></span><span class="line"><span class="cl">sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/02-install-docker-ce-wsl2.png"
         alt="Installing Docker CE and containerd in Ubuntu WSL2"/> <figcaption>
            <p>Configuring the official Docker CE repository and installing containerd and the Docker daemon inside Ubuntu WSL2.</p>
        </figcaption>
</figure>

<p>Verify that both the Docker daemon and containerd services are running cleanly under systemd:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl status docker
</span></span><span class="line"><span class="cl">systemctl status containerd
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/03-docker-containerd-service-status.png"
         alt="Verifying systemd status for Docker and containerd"/> <figcaption>
            <p>Confirming active (running) status for both docker.service and containerd.service under WSL2 systemd.</p>
        </figcaption>
</figure>

<p>Before moving to Kubernetes, sanity check that native Docker has full GPU access through the WSL2 NVIDIA container runtime:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker container run --gpus all --rm nvidia/cuda:13.3.1-base-ubuntu26.04 nvidia-smi -L
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/04-docker-gpu-passthrough-verification.png"
         alt="Docker container GPU passthrough test"/> <figcaption>
            <p>Direct GPU passthrough test: Docker successfully identifies GPU 0 as the NVIDIA GeForce RTX 4070 Laptop GPU.</p>
        </figcaption>
</figure>

<hr>
<h2 id="run-minikube-with-nvidia-gpu-support-on-wsl2">Run Minikube with NVIDIA GPU Support on WSL2</h2>
<p>Install the latest Minikube Debian package:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube_latest_amd64.deb
</span></span><span class="line"><span class="cl">sudo dpkg -i minikube_latest_amd64.deb
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/05-install-minikube-deb.png"
         alt="Installing Minikube package in WSL2"/> <figcaption>
            <p>Installing Minikube v1.39.0 via Debian package.</p>
        </figcaption>
</figure>

<p>Following the official <a href="https://minikube.sigs.k8s.io/docs/tutorials/nvidia/">Minikube GPU documentation</a>, starting Minikube with GPU acceleration requires specific driver and runtime parameters. Now start Minikube. Two flags matter here and both are easy to miss:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">minikube start <span class="se">\
</span></span></span><span class="line"><span class="cl">  --driver<span class="o">=</span>docker <span class="se">\
</span></span></span><span class="line"><span class="cl">  --container-runtime<span class="o">=</span>docker <span class="se">\
</span></span></span><span class="line"><span class="cl">  --gpus<span class="o">=</span>all <span class="se">\
</span></span></span><span class="line"><span class="cl">  --memory<span class="o">=</span>12g <span class="se">\
</span></span></span><span class="line"><span class="cl">  --cpus<span class="o">=</span><span class="m">10</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --extra-config<span class="o">=</span>apiserver.service-account-issuer<span class="o">=</span>https://kubernetes.default.svc <span class="se">\
</span></span></span><span class="line"><span class="cl">  --extra-config<span class="o">=</span>apiserver.service-account-signing-key-file<span class="o">=</span>/var/lib/minikube/certs/sa.key
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/06-minikube-start-gpu-flags.png"
         alt="Starting Minikube with GPU support and Docker runtime"/> <figcaption>
            <p>Minikube starting Kubernetes v1.37.0 on Docker, automatically detecting and enabling the nvidia-device-plugin addon.</p>
        </figcaption>
</figure>

<blockquote>
<p>[!IMPORTANT]
<strong>Why <code>--container-runtime=docker</code> is required:</strong>
By default, even when using <code>--driver=docker</code>, Minikube configures containerd as its internal in-node runtime. However, Minikube&rsquo;s <code>--gpus=all</code> flag currently hooks into Docker&rsquo;s OCI runtime wrapper. If you omit <code>--container-runtime=docker</code>, Minikube fails during preflight checks with an invalid flag combination error.</p>
</blockquote>
<p>Notice the startup output: Minikube automatically detects the GPU and enables the <code>nvidia-device-plugin</code> addon for you.</p>
<hr>
<h2 id="verifying-the-cluster-and-gpu-allocation">Verifying the Cluster and GPU Allocation</h2>
<p>Install <code>kubectl</code> to talk to the cluster:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -LO <span class="s2">&#34;https://dl.k8s.io/release/</span><span class="k">$(</span>curl -L -s https://dl.k8s.io/release/stable.txt<span class="k">)</span><span class="s2">/bin/linux/amd64/kubectl&#34;</span>
</span></span><span class="line"><span class="cl">sudo install -o root -g root -m <span class="m">0755</span> kubectl /usr/local/bin/kubectl
</span></span><span class="line"><span class="cl">kubectl version --client
</span></span><span class="line"><span class="cl">kubectl get nodes
</span></span><span class="line"><span class="cl">kubectl get pods -A
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/07-kubectl-install-and-cluster-verification.png"
         alt="Kubectl verification and nvidia-device-plugin DaemonSet"/> <figcaption>
            <p>Node minikube is Ready (v1.37.0) and the nvidia-device-plugin-daemonset is 1/1 Running in kube-system.</p>
        </figcaption>
</figure>

<p>Next, inspect the node&rsquo;s schedulable capacity:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl describe node minikube <span class="p">|</span> grep -A7 <span class="s2">&#34;Capacity:\|Allocatable:&#34;</span>
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/08-minikube-node-gpu-capacity.png"
         alt="Minikube node capacity showing GPU resource"/> <figcaption>
            <p>Cluster verification: nvidia.com/gpu: 1 is now officially registered in Capacity and Allocatable.</p>
        </figcaption>
</figure>

<p>A quick test pod confirms the GPU is reachable from inside Kubernetes:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat <span class="s">&lt;&lt;EOF | kubectl apply -f -
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: Pod
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s">  name: gpu-test
</span></span></span><span class="line"><span class="cl"><span class="s">spec:
</span></span></span><span class="line"><span class="cl"><span class="s">  restartPolicy: Never
</span></span></span><span class="line"><span class="cl"><span class="s">  containers:
</span></span></span><span class="line"><span class="cl"><span class="s">    - name: cuda-test
</span></span></span><span class="line"><span class="cl"><span class="s">      image: nvidia/cuda:13.3.1-base-ubuntu26.04
</span></span></span><span class="line"><span class="cl"><span class="s">      command: [&#34;nvidia-smi&#34;, &#34;-L&#34;]
</span></span></span><span class="line"><span class="cl"><span class="s">      resources:
</span></span></span><span class="line"><span class="cl"><span class="s">        limits:
</span></span></span><span class="line"><span class="cl"><span class="s">          nvidia.com/gpu: 1
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>Check the test pod logs:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl logs gpu-test
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/09-k8s-cuda-gpu-test-pod.png"
         alt="CUDA test pod running on Minikube"/> <figcaption>
            <p>Test pod running nvidia-smi inside the Kubernetes cluster and successfully accessing the RTX 4070.</p>
        </figcaption>
</figure>

<p>Real GPU scheduling and <code>nvidia.com/gpu</code> resource accounting, without Docker Desktop getting in the way.</p>
<hr>
<h2 id="deploy-vllm-on-kubernetes-with-nvidia-gpu">Deploy vLLM on Kubernetes with NVIDIA GPU</h2>
<p>Model choice for 8 GB VRAM: <strong>Qwen3.5-0.8B</strong> (<code>Qwen/Qwen3.5-0.8B</code>). Small enough to leave real headroom for vLLM&rsquo;s KV cache, which is the main reason to use vLLM over llama.cpp or Ollama in the first place, and it is a capable model at that size. As detailed in the <a href="https://docs.vllm.ai/">vLLM documentation</a>, the server exposes an OpenAI-compatible API endpoint over HTTP.</p>
<h3 id="the-shared-memory-devshm-gotcha">The Shared Memory (<code>/dev/shm</code>) Gotcha</h3>
<p>When running vLLM via the Docker CLI, passing <code>--ipc=host</code> lets workers exchange tensors and state across processes using host shared memory. In Kubernetes, pods do not share the host IPC namespace by default, and Kubernetes provisions <code>/dev/shm</code> as a minimal 64 MB tmpfs mount.</p>
<p>Some PyTorch and vLLM multiprocessing workloads can require significantly more shared memory than Kubernetes&rsquo; default <code>/dev/shm</code> (which defaults to a minimal 64 MB tmpfs mount). If insufficient shared memory is available, initialization or worker processes may fail. The Kubernetes solution is mounting an <code>emptyDir</code> volume backed by host RAM (<code>medium: Memory</code>) with a dedicated <code>sizeLimit</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># vllm-qwen-k8s.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">vllm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">vllm/vllm-openai:v0.28.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">args</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="s2">&#34;--model=Qwen/Qwen3.5-0.8B&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="s2">&#34;--gpu-memory-utilization=0.7&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="s2">&#34;--max-model-len=8192&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">nvidia.com/gpu</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">hf-cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/root/.cache/huggingface</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dshm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/dev/shm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">hf-cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">emptyDir</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dshm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">emptyDir</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">medium</span><span class="p">:</span><span class="w"> </span><span class="l">Memory</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">sizeLimit</span><span class="p">:</span><span class="w"> </span><span class="l">2Gi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterIP</span><span class="w">
</span></span></span></code></pre></div><p>Two practical notes on this manifest:</p>
<ul>
<li><strong>Image version pinning:</strong> I pin the image version (<code>v0.28.0</code>) here so the deployment remains reproducible. You can update the image tag after validating compatibility with your CUDA and model version.</li>
<li><strong>Hugging Face cache:</strong> <code>emptyDir</code> is used here for simplicity. The Hugging Face model cache is lost when the pod is recreated. For repeated testing or larger models, consider using a PersistentVolumeClaim (PVC) or a host-mounted path.</li>
</ul>
<p>Apply the manifest and watch the deployment roll out:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f vllm-qwen-k8s.yaml
</span></span><span class="line"><span class="cl">kubectl get pods,svc
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/10-vllm-deployment-and-service-running.png"
         alt="vLLM deployment and service running"/> <figcaption>
            <p>Applying vllm-qwen-k8s.yaml and confirming the vLLM pod reaches 1/1 Running status alongside the ClusterIP service.</p>
        </figcaption>
</figure>

<p>Stream the pod logs to inspect the vLLM initialization sequence:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl logs vllm-qwen-5c8bbc786f-gdpmj -f
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/11-vllm-pod-startup-logs.png"
         alt="vLLM pod engine initialization logs"/> <figcaption>
            <p>vLLM v0.28.0 engine initialization: resolving Qwen3_5ForConditionalGeneration, setting max model length to 8192, and warming up CUDA graphs.</p>
        </figcaption>
</figure>

<p>Once PyTorch compilation and CUDA graph capture finish, the ASGI application completes startup and begins listening on port 8000:</p>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/12-vllm-server-ready-and-metrics.png"
         alt="vLLM application startup complete and request logs"/> <figcaption>
            <p>vLLM application startup complete: exposing OpenAI-compatible endpoints (/v1/chat/completions) with live throughput metrics.</p>
        </figcaption>
</figure>

<hr>
<h2 id="testing-inference-and-hardware-telemetry">Testing Inference and Hardware Telemetry</h2>
<p>Forward port 8000 from the cluster service to the local machine:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl port-forward svc/vllm-qwen 8000:8000
</span></span></code></pre></div><p>Now issue an OpenAI-standard chat completion request using <code>curl</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl http://localhost:8000/v1/chat/completions <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;model&#34;: &#34;Qwen/Qwen3.5-0.8B&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;messages&#34;: [{&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;Say hello in one sentence.&#34;}]
</span></span></span><span class="line"><span class="cl"><span class="s1">  }&#39;</span>
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/13-vllm-curl-chat-completion-response.png"
         alt="Calling vLLM chat completions API with curl"/> <figcaption>
            <p>Inference test: 48 tokens generated cleanly through the OpenAI-compatible vLLM API running in Minikube.</p>
        </figcaption>
</figure>

<p>The response returned without issues, with server logs showing generation throughput averaging 23.5 tokens/s.</p>
<p>Finally, checking Windows Task Manager confirms the hardware utilization on the host:</p>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/14-windows-task-manager-gpu-vram-usage.png"
         alt="Windows Task Manager GPU memory usage"/> <figcaption>
            <p>Windows Task Manager telemetry: 4.4 GB of 8.0 GB dedicated VRAM allocated to the WSL2 vLLM pod at 45°C.</p>
        </figcaption>
</figure>

<p>Dedicated GPU memory held at <strong>4.4 / 8.0 GB</strong> (in line with the <code>--gpu-memory-utilization 0.7</code> setting plus desktop display overhead), with the GPU temperature at 45°C.</p>
<hr>
<h2 id="beyond-local-dev-why-production-clusters-use-the-nvidia-gpu-operator">Beyond Local Dev: Why Production Clusters Use the NVIDIA GPU Operator</h2>
<p>For a single-node laptop or a Minikube sandbox, managing drivers on the host and running a standalone device plugin DaemonSet gets the job done.</p>
<p>In production Kubernetes clusters, manual node configuration does not scale. When you operate multi-node clusters across cloud providers or bare metal, nodes get provisioned dynamically by autoscalers, Linux kernel patch levels drift, and different GPU architectures (such as A100, H100, or L40S) coexist in the same cluster.</p>
<p>This is why production setups deploy the <a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/">NVIDIA GPU Operator</a> instead of managing the device plugin directly.</p>
<p>The GPU Operator automates the deployment and lifecycle management of the NVIDIA software stack required for GPU-enabled Kubernetes nodes. Driven by the <code>ClusterPolicy</code> Custom Resource Definition, it manages several components depending on your cluster configuration:</p>
<ol>
<li><strong>Driver Management:</strong> Depending on the operating environment, the Operator can compile and load the NVIDIA driver container matched to the node kernel, or use pre-installed host drivers (common in managed Kubernetes environments like GKE, EKS, or AKS).</li>
<li><strong>Container Toolkit and CDI Configuration:</strong> Depending on the Operator version and runtime configuration, GPU injection can be configured through direct NVIDIA runtime integration or CDI-based device injection (Container Device Interface), which modern releases enable by default.</li>
<li><strong>Device Plugin Supervision:</strong> It deploys and manages the lifecycle of the NVIDIA Kubernetes Device Plugin DaemonSet, keeping it in sync with driver and runtime availability.</li>
<li><strong>Node Feature Discovery (GFD):</strong> It automatically detects physical GPU capabilities and labels each node (such as <code>nvidia.com/gpu.product: NVIDIA-A100-SXM4-80GB</code> or <code>nvidia.com/gpu.family: ampere</code>). Workloads can then target specific GPU models using standard Kubernetes <code>nodeSelector</code> or affinity rules.</li>
<li><strong>Cluster Metrics with DCGM Exporter:</strong> It runs the Data Center GPU Manager (DCGM) exporter to collect metrics (GPU compute utilization, VRAM usage, temperature, power draw, and memory bandwidth) and expose them to Prometheus for cluster monitoring and alerting.</li>
<li><strong>MIG Management:</strong> On enterprise GPUs that support Multi-Instance GPU (MIG), the Operator can dynamically partition a single physical GPU into isolated hardware instances, allowing smaller workloads to share an A100 or H100 without memory interference.</li>
</ol>
<p>In short: the device plugin provides basic GPU scheduling for pods. The GPU Operator manages the operational lifecycle, monitoring, and driver stack required to run GPU infrastructure reliably in production.</p>
<hr>
<h2 id="key-takeaways">Key Takeaways</h2>
<ol>
<li><strong>Docker Desktop Kubernetes isolates nodes without GPU passthrough:</strong> In tested setups, <code>desktop-control-plane</code> runs in a nested <code>sysbox-runc</code> sandbox without GPU device passthrough flags.</li>
<li><strong>Minikube on native Docker CE in WSL2 works reliably:</strong> Launching Minikube with <code>--driver=docker --container-runtime=docker --gpus=all</code> automatically activates the NVIDIA device plugin addon.</li>
<li><strong>Understand device plugin vs node prerequisites:</strong> The device plugin only manages discovery and Kubelet allocation. Host drivers, Container Toolkit, and runtime configuration must already exist on the worker node.</li>
<li><strong>Always size <code>/dev/shm</code> in Kubernetes:</strong> Multiprocessing in PyTorch and vLLM quickly exceeds the default 64 MB tmpfs mount. Mount an <code>emptyDir</code> memory volume backed by host RAM.</li>
<li><strong>Budget VRAM carefully:</strong> Setting <code>--gpu-memory-utilization 0.7</code> on a compact model like Qwen3.5-0.8B preserves headroom for the KV cache on an 8 GB GPU.</li>
<li><strong>Deploy the NVIDIA GPU Operator in production:</strong> Production multi-node clusters rely on the GPU Operator to manage drivers, CDI device injection, node feature discovery, and DCGM monitoring.</li>
</ol>
<hr>
<p>Thanks for reading! If you run into issues setting up GPU passthrough on your machine, have questions, or have suggestions for other local AI setups to explore, leave a comment below.</p>
]]></content:encoded></item><item><title>Kubernetes is Adding a K to YAML: Why KYAML Makes Sense</title><link>https://vishctl.dev/posts/kubernetes-kyaml/</link><pubDate>Sun, 06 Sep 2026 03:00:00 +0800</pubDate><guid>https://vishctl.dev/posts/kubernetes-kyaml/</guid><category>kubernetes</category><category>yaml</category><category>devops</category><description>Kubernetes 1.37 makes KYAML stable: how flow style eliminates silent type coercion, removes load-bearing indentation, and how to try it.</description><content:encoded><![CDATA[<p>Kubernetes 1.37 (&ldquo;Garhwal&rdquo;) just made KYAML Stable. YAML with a K bolted on, and it&rsquo;s a genuinely useful feature.</p>
<h2 id="the-problem">The problem</h2>
<p>YAML has two well known landmines.</p>
<p><strong>Type coercion.</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">country</span><span class="p">:</span><span class="w"> </span><span class="kc">NO</span><span class="w">      </span><span class="c"># becomes boolean false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">yes</span><span class="w">     </span><span class="c"># becomes boolean true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="m">3.10</span><span class="w">    </span><span class="c"># becomes float 3.1</span><span class="w">
</span></span></span></code></pre></div><p>Those values were meant to be strings. YAML quietly turned them into something else.</p>
<p><strong>Whitespace as structure.</strong> Indentation defines meaning in normal YAML. Get one space wrong and the file still parses. It just means something else now, silently.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP  </span><span class="w"> </span><span class="c"># one space off, this attaches to the wrong container</span><span class="w">
</span></span></span></code></pre></div><p>No error. No warning. Just wrong.</p>
<p>Helm makes this worse, since you&rsquo;re injecting indentation from outside the YAML context and hoping it lines up.</p>
<h2 id="what-kyaml-actually-changes">What KYAML actually changes</h2>
<p>Nothing about the language. It&rsquo;s YAML&rsquo;s existing &ldquo;flow style,&rdquo; just enforced. Every KYAML file is valid YAML. Old tooling, old parsers, old <code>kubectl</code> versions all read it fine.</p>
<p>Three rules: objects use <code>{ }</code>, arrays use <code>[ ]</code>, strings are always quoted. Numbers and booleans stay bare, since they&rsquo;re actually meant to be those types.</p>
<p>One example, everything at once:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># before</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">api</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">automountServiceAccountToken</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">hostNetwork</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">REGION</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="kc">NO</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># after (KYAML)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;apps/v1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Deployment&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;api&#34;</span><span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w"> </span>{<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w"> </span>{<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w"> </span>{<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">automountServiceAccountToken</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">hostNetwork</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>{<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;app&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;nginx&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>{<span class="w"> </span><span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w"> </span>}<span class="p">],</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">env</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>{<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;REGION&#34;</span><span class="nt">, value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;NO&#34;</span><span class="w"> </span>}<span class="p">],</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>}<span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="p">],</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>}<span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>}<span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl">}<span class="w">
</span></span></span></code></pre></div><p>Everything in one shot:</p>
<ul>
<li><code>{ }</code> for objects (<code>metadata</code>, <code>spec</code>, each container)</li>
<li><code>[ ]</code> for arrays (<code>containers</code>, <code>ports</code>, <code>env</code>)</li>
<li>strings always quoted (<code>&quot;api&quot;</code>, <code>&quot;nginx&quot;</code>, <code>&quot;REGION&quot;</code>)</li>
<li><code>replicas: 2</code> and <code>containerPort: 8080</code> stay bare, they&rsquo;re real numbers</li>
<li><code>automountServiceAccountToken: false</code> and <code>hostNetwork: true</code> stay bare, they&rsquo;re real booleans</li>
<li><code>value: &quot;NO&quot;</code> is quoted, because it&rsquo;s a string that would otherwise get read as <code>false</code></li>
</ul>
<p>KYAML doesn&rsquo;t remove types, it just stops guessing which one you meant.</p>
<h2 id="why-it-matters">Why it matters</h2>
<p>Since structure comes from <code>{ }</code> and <code>[ ]</code>, not from spaces, indentation stops being load bearing. Squash a KYAML file onto one line and it means exactly the same thing. That kills the Helm whitespace problem outright.</p>
<h2 id="trying-it">Trying it</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl get deployment api -o kyaml
</span></span></code></pre></div><p>Convert existing files:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">go install github.com/google/yamlfmt/cmd/yamlfmt@latest
</span></span><span class="line"><span class="cl">yamlfmt --kyaml manifest.yaml
</span></span></code></pre></div><p>Also ships as a pre-commit hook and a Docker image for CI.</p>
<p>Nothing forces the switch. Plain YAML keeps working. Mixed repos, some converted, some not, are fine.</p>
<p>My plan if I touch this on a real repo: one directory first, verify round-trip parsing, check Helm and Kustomize don&rsquo;t reformat it differently, then expand once it&rsquo;s boring.</p>
<p>Not required. Cheap upgrade. Worth exploring.</p>
<p>For a broader breakdown of what else landed in this release, check out my notes on <a href="https://vishctl.dev/posts/kubernetes-v1-37-stability/">Kubernetes v1.37: An Operator&rsquo;s Look at Garhwal</a>.</p>
]]></content:encoded></item><item><title>Kubernetes v1.37: An Operator's Look at Garhwal</title><link>https://vishctl.dev/posts/kubernetes-v1-37-stability/</link><pubDate>Wed, 02 Sep 2026 20:30:00 +0800</pubDate><guid>https://vishctl.dev/posts/kubernetes-v1-37-stability/</guid><category>kubernetes</category><category>devops</category><category>cloudnative</category><category>infrastructure</category><description>My notes on Kubernetes v1.37: native HPA scale-to-zero, tracking unused PVCs, ClusterTrustBundles, and etcd streaming.</description><content:encoded><![CDATA[<p>Kubernetes v1.37 landed recently under the release theme <strong>Garhwal</strong>, named after the Himalayan region of Uttarakhand, India. The release packages 67 enhancements across alpha, beta, and stable.</p>
<p>The upstream changelog is massive, but as someone who spends most of the day managing control planes, dealing with storage, and watching cloud bills, only a handful of changes stand out as immediately relevant to day-to-day work.</p>
<p>Here are the features I am keeping an eye on, along with practical examples for how they work.</p>
<h3 id="1-hpa-can-finally-scale-to-zero-minreplicas-0">1. HPA Can Finally Scale to Zero (<code>minReplicas: 0</code>)</h3>
<p>For years, one of the most frustrating limitations in vanilla Kubernetes autoscaling was that <code>HorizontalPodAutoscaler</code> could not scale below one replica. If you had a queue consumer waiting on messages in SQS or RabbitMQ, or a GPU worker waiting on batch jobs, you had to keep at least one pod running around the clock, or pull in third-party tools like KEDA just to do basic scale-to-zero.</p>
<p>In v1.37, <strong>HPA scale to zero</strong> graduated to Beta and is enabled by default. You can now configure <code>minReplicas: 0</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">autoscaling/v2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">HorizontalPodAutoscaler</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">queue-worker-hpa</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">scaleTargetRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">queue-worker</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">minReplicas</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">maxReplicas</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">metrics</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">External</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">external</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">metric</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">aws_sqs_approximate_number_of_messages_visible</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">target</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">Value</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">averageValue</span><span class="p">:</span><span class="w"> </span><span class="m">30</span><span class="w">
</span></span></span></code></pre></div><p>When the queue is empty, the controller scales the deployment down to 0 pods:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl get hpa queue-worker-hpa
</span></span><span class="line"><span class="cl"><span class="c1"># NAME               REFERENCE                 TARGETS        MINPODS   MAXPODS   REPLICAS   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># queue-worker-hpa   Deployment/queue-worker   0/30 (avg)     0         10        0          18m</span>
</span></span></code></pre></div><p>Under the hood, the HPA controller adds a <code>ScaledToZero</code> condition so you can easily distinguish an automated scale-down from a manual shutdown:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">status</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">currentReplicas</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">desiredReplicas</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">conditions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">ScaledToZero</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">status</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;True&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">reason</span><span class="p">:</span><span class="w"> </span><span class="l">ScaledToZero</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">message</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;all pods scaled down based on metrics&#34;</span><span class="w">
</span></span></span></code></pre></div><p><strong>The catch:</strong> This only works with object or external metrics. It does not work with CPU or memory metrics, because a deployment with zero running pods produces no CPU or memory data to trigger an upscale. For event-driven workers and GPU workloads, this saves real money on cloud bills.</p>
<h3 id="2-finding-orphaned-pvcs-just-got-much-easier">2. Finding Orphaned PVCs Just Got Much Easier</h3>
<p>PersistentVolumeClaims have a habit of outliving the workloads that created them. Someone deletes a Deployment or StatefulSet, but the PVC stays behind in the namespace. Weeks or months later, dozens of unused EBS volumes or cloud disks are quietly burning infrastructure budget.</p>
<p>Until now, Kubernetes gave you no native way to know when a PVC was last used without writing custom scripts to inspect every pod spec in the cluster.</p>
<p>Kubernetes v1.37 addresses this by graduating <strong>PVC last-used tracking</strong> to Beta (enabled by default). The PVC protection controller now adds an <code>Unused</code> condition to <code>status.conditions</code> on PersistentVolumeClaims:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">PersistentVolumeClaim</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">abandoned-redis-data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">analytics</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">status</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">phase</span><span class="p">:</span><span class="w"> </span><span class="l">Bound</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">conditions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">Unused</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">status</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;True&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">reason</span><span class="p">:</span><span class="w"> </span><span class="l">NoPodsUsingPVC</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">lastTransitionTime</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2026-08-15T09:30:00Z&#34;</span><span class="w">
</span></span></span></code></pre></div><ul>
<li>When the last pod referencing a PVC terminates, <code>Status</code> flips to <code>&quot;True&quot;</code> with reason <code>NoPodsUsingPVC</code>.</li>
<li>The <code>lastTransitionTime</code> tells you the exact timestamp since the volume became idle.</li>
<li>As soon as a new pod attaches, the condition flips back to <code>False</code>.</li>
</ul>
<p>Kubernetes does not delete anything automatically (which is good; you do not want the control plane guessing whether data is safe to purge). But this gives you a clean field to query across your cluster:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># List all PVCs across all namespaces that are currently unused, with their idle timestamp</span>
</span></span><span class="line"><span class="cl">kubectl get pvc -A -o custom-columns<span class="o">=</span><span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;NAMESPACE:.metadata.namespace,\
</span></span></span><span class="line"><span class="cl"><span class="s1">NAME:.metadata.name,\
</span></span></span><span class="line"><span class="cl"><span class="s1">CAPACITY:.status.capacity.storage,\
</span></span></span><span class="line"><span class="cl"><span class="s1">UNUSED:.status.conditions[?(@.type==&#34;Unused&#34;)].status,\
</span></span></span><span class="line"><span class="cl"><span class="s1">IDLE_SINCE:.status.conditions[?(@.type==&#34;Unused&#34;)].lastTransitionTime&#39;</span>
</span></span></code></pre></div><p>With one command, you can spot storage that has been abandoned for 30+ days and clean it up safely.</p>
<h3 id="3-clustertrustbundles-go-ga-retire-your-ca-sync-cronjobs">3. ClusterTrustBundles Go GA: Retire Your CA Sync Cronjobs</h3>
<p>If you manage enterprise clusters with internal PKI, private container registries, or corporate proxies, you have almost certainly solved the root certificate problem with a workaround. Most teams either run a custom controller that clones a secret into every namespace as a <code>ConfigMap</code>, or bake internal certificates into container base images.</p>
<p>In v1.37, <strong>ClusterTrustBundles</strong> and <strong>Pod Certificates</strong> officially graduated to Stable (GA) under <code>certificates.k8s.io</code>.</p>
<p>A <code>ClusterTrustBundle</code> is a cluster-scoped object that holds trusted X.509 certificate anchors:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">certificates.k8s.io/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterTrustBundle</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">corporate-root-ca</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">signerName</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;corp.internal/pki&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">trustBundle</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">    -----BEGIN CERTIFICATE-----
</span></span></span><span class="line"><span class="cl"><span class="sd">    MIICjTCCAjSgAwIBAgIUTkX...
</span></span></span><span class="line"><span class="cl"><span class="sd">    -----END CERTIFICATE-----</span><span class="w">
</span></span></span></code></pre></div><p>Workloads in any namespace can project this bundle directly into their filesystem via standard projected volumes:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">internal-api</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">my-registry.internal/api:v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="c"># Point common TLS libraries to the projected CA bundle</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">SSL_CERT_FILE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/ssl/certs/corporate-root-ca.crt</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">trusted-certs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/ssl/certs/corporate-root-ca.crt</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">subPath</span><span class="p">:</span><span class="w"> </span><span class="l">corporate-root-ca.crt</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">readOnly</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">trusted-certs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">projected</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">sources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>- <span class="nt">clusterTrustBundle</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">corporate-root-ca</span><span class="w">
</span></span></span></code></pre></div><p>The kubelet handles mounting and rotation natively. That is one less custom controller or sync cronjob to maintain.</p>
<h3 id="4-control-plane-breathing-room-etcd-rangestream--concurrent-decode">4. Control Plane Breathing Room: etcd RangeStream &amp; Concurrent Decode</h3>
<p>When large clusters experience network hiccups or controller restarts, hundreds of controllers can hit the API server with list requests at the exact same moment. Historically, etcd built full responses in memory before sending them, and the API server processed watch events serially on a single goroutine. During recovery storms, control plane memory would spike, occasionally sending nodes into Out-Of-Memory (OOM) loops.</p>
<p>In v1.37, two features land on by default to fix this:</p>
<ol>
<li><strong>etcd RangeStream (Beta):</strong> Instead of buffering massive key-value sets in memory, etcd streams responses back in chunks. This turns what used to be a memory-heavy dump into a smooth stream.</li>
<li><strong>Concurrent Watch Object Decode (Beta):</strong> The API server now decodes incoming watch events across a pool of worker goroutines rather than processing them one by one.</li>
</ol>
<p>If you interact with etcd 3.7+ directly, you can test the new chunked streaming RPC via <code>etcdctl</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Stream keys in chunks rather than buffering everything into a single gRPC response</span>
</span></span><span class="line"><span class="cl">etcdctl get /registry/pods/ --prefix --stream
</span></span></code></pre></div><p>In upstream benchmarks across 150,000 pods, these two improvements combined cut cache initialization time by more than 50%. For operators of large clusters or CRD-heavy environments, this means significantly less control plane jitter during rolling restarts.</p>
<h3 id="5-kubectl-get--o-kyaml-goes-stable">5. <code>kubectl get -o kyaml</code> Goes Stable</h3>
<p>YAML has plenty of parsing quirks (such as unquoted country codes like <code>NO</code> being interpreted as boolean <code>false</code>).</p>
<p>In v1.37, <strong>KYAML</strong> reaches Stable. KYAML is a safer, unambiguous subset of YAML tailored specifically for Kubernetes. You do not need to change any of your existing manifests or pipelines; KYAML is strictly backwards compatible with standard YAML.</p>
<p>You can now run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Output clean, normalized YAML without parser ambiguity</span>
</span></span><span class="line"><span class="cl">kubectl get deployment queue-worker -o kyaml
</span></span></code></pre></div><p>Comparing the output against regular YAML:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Standard YAML output often strips quotes from country codes or booleans:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">country</span><span class="p">:</span><span class="w"> </span><span class="kc">NO</span><span class="w">    </span><span class="c"># Parsed by some YAML 1.1 loaders as boolean false!</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># KYAML output strictly normalizes and quotes scalar types:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">country</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;NO&#34;</span><span class="w">
</span></span></span></code></pre></div><p>It formats cleanly, avoids parser edge cases, and provides predictable output for gitops diffs and scripts.</p>
<h3 id="6-selinuxmount-reaches-ga">6. SELinuxMount Reaches GA</h3>
<p>If your nodes run SELinux (common in Red Hat, Rocky, or Fedora environments), volume mounts historically required recursive file relabeling on every pod start, which could cause painfully slow startup times on large volumes.</p>
<p>In v1.37, <strong>SELinuxMount</strong> is now Stable and enabled by default for CSI drivers that support it. Volumes are mounted with a single mount context rather than recursively walking the directory tree.</p>
<p><strong>The catch:</strong> A volume mount can only carry one SELinux context. If you run multiple pods with different SELinux labels that share the same volume on the same node, those pods may now fail to mount.</p>
<p>If your workloads rely on shared volumes with mixed labels, you can preserve the legacy recursive relabeling behavior in your Pod spec:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Pod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">shared-data-consumer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">securityContext</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">seLinuxChangePolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Recursive</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">alpine</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">shared-storage</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">shared-storage</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">persistentVolumeClaim</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">claimName</span><span class="p">:</span><span class="w"> </span><span class="l">shared-data-pvc</span><span class="w">
</span></span></span></code></pre></div><h3 id="where-this-leaves-us">Where This Leaves Us</h3>
<p>Do not rush to upgrade production this weekend. If you use a managed provider (EKS, GKE, Rancher), wait for <code>.1</code> or <code>.2</code> patch releases. If you run physical data centers like me and manage the control plane yourself, be even more conservative: test locally, promote through lower environments, and comfortably stay on n-1 or n-2.</p>
<p>The real reason to track v1.37 today is preventing fresh technical debt. If your team was about to build custom controllers to sync CA certs or write scripts to hunt dead storage, tell them to hold off. Upstream is solving it natively.</p>
<p>Let the early patches bake, keep your clusters healthy, and start planning which legacy workarounds you get to delete.</p>
]]></content:encoded></item><item><title>Hello World</title><link>https://vishctl.dev/posts/hello-world/</link><pubDate>Wed, 02 Sep 2026 15:17:58 +0800</pubDate><guid>https://vishctl.dev/posts/hello-world/</guid><category>devops</category><category>kubernetes</category><category>infrastructure</category><category>ai</category><category>homelab</category><description>A quick intro to vishctl, my background in Kubernetes and AI infra, and why this blog exists.</description><content:encoded><![CDATA[<p>Hey, I&rsquo;m Vishnu. Welcome to my little corner of the internet.</p>
<h3 id="what-i-do">What I Do</h3>
<p>Professionally, I have about 9 years of overall experience under my belt. I actually started out writing code as a .NET developer before pivoting hard into DevOps and infrastructure engineering. Since 2019, I have been deep in the Kubernetes ecosystem, architecting and managing infrastructure across physical data centers and public clouds.</p>
<p>Over the years, that has meant running everything from standard high-traffic web platforms to mission-critical telco workloads, airgapped enterprise environments, and AI-native GPU clusters. Nowadays, my work revolves heavily around container orchestration, SUSE and Rancher setups, vLLM inference deployments, and homelab GPU testing.</p>
<h3 id="who-i-am">Who I Am</h3>
<p>At my core, I am a tech guy through and through. Whether it is tackling an infrastructure puzzle at work, geeking out over the latest gadgets, or dreaming of walking the floor at CES, technology is simply what makes the most sense to me.</p>
<p>Outside of terminal windows and YAML files, you will usually find me playing FPS PC games, binging a good web series, or unwinding with music. I also have a massive bucket list goal: to travel the entire world before I hit my deathbed.</p>
<h3 id="why-this-blog-exists">Why This Blog Exists</h3>
<p>This site is where I will document the problems I run into and the solutions that worked. Expect practical write-ups on Kubernetes quirks, AI infrastructure experiments, airgapped deployments, and whatever interesting challenge I am currently knee-deep in.</p>
<p>No fixed publishing schedule. No corporate polish guarantee. Just authentic notes from the trenches, mostly so future-me (and maybe you) does not have to re-solve the same headache twice.</p>
<p>More soon.</p>
]]></content:encoded></item></channel></rss>