<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hamdi (KHELIL) LION</title>
    <description>The latest articles on DEV Community by Hamdi (KHELIL) LION (@hkhelil).</description>
    <link>https://dev.to/hkhelil</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1930768%2Fa35af795-ee57-49bc-aa3a-3dd7906953e3.jpeg</url>
      <title>DEV Community: Hamdi (KHELIL) LION</title>
      <link>https://dev.to/hkhelil</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hkhelil"/>
    <language>en</language>
    <item>
      <title>🤖 Kubernetes as Your AI Control Plane: Running Claude and Ollama Agents with kagent 🧠</title>
      <dc:creator>Hamdi (KHELIL) LION</dc:creator>
      <pubDate>Fri, 03 Jul 2026 06:12:43 +0000</pubDate>
      <link>https://dev.to/hkhelil/kubernetes-as-your-ai-control-plane-running-claude-and-ollama-agents-with-kagent-1bmb</link>
      <guid>https://dev.to/hkhelil/kubernetes-as-your-ai-control-plane-running-claude-and-ollama-agents-with-kagent-1bmb</guid>
      <description>&lt;h2&gt;
  
  
  👋 Hey there
&lt;/h2&gt;

&lt;p&gt;If you run a homelab, you already have a Kubernetes cluster sitting there humming away. So why send every AI workload off to a SaaS dashboard when you can run agents right next to your pods, with the same kubectl and GitOps flow you already trust?&lt;/p&gt;

&lt;p&gt;That is exactly what kagent gives us. It is a CNCF sandbox project (started at Solo.io, built by folks from the Istio world) that turns AI agents into first class Kubernetes workloads. Agents, models, and tools all become custom resources you apply with &lt;code&gt;kubectl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;👉 The big idea: kagent puts one clean abstraction between your agents and your models, so you can point the same agent at Claude in the cloud or at Ollama running in your own cluster, and switch by changing a single field.&lt;/p&gt;

&lt;p&gt;In this guide we will build a small but real inference stack in a homelab cluster, wired to both.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧱 What we will cover
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ What the kagent inference stack actually looks like&lt;/li&gt;
&lt;li&gt;✅ Installing kagent with Helm&lt;/li&gt;
&lt;li&gt;✅ Wiring up Claude for cloud inference (the ModelConfig resource)&lt;/li&gt;
&lt;li&gt;✅ Deploying Ollama inside the cluster for fully local, on prem inference&lt;/li&gt;
&lt;li&gt;✅ Building agents that use each model with real Kubernetes tools&lt;/li&gt;
&lt;li&gt;✅ Swapping models with a one line change&lt;/li&gt;
&lt;li&gt;✅ Adding a human approval gate so your agent never nukes the wrong namespace&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🗺️ The inference stack, layer by layer
&lt;/h2&gt;

&lt;p&gt;Before we touch YAML, here is the mental model. In kagent your "inference stack" is just a few layers, each a Kubernetes resource or component:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Model provider: where the tokens come from. Claude via the Anthropic API, or Ollama serving a local model.&lt;/li&gt;
&lt;li&gt;✅ ModelConfig (CRD): tells kagent which provider, which model, and which secret holds the key.&lt;/li&gt;
&lt;li&gt;✅ Agent (CRD): a system prompt plus a set of tools plus a reference to a ModelConfig.&lt;/li&gt;
&lt;li&gt;✅ Tools over MCP: kagent ships a tool server that exposes Kubernetes actions as tools your agent can call.&lt;/li&gt;
&lt;li&gt;✅ Controller and runtime: the Go controller watches the CRDs, and the runtime (Agent Development Kit) runs the agent loop.&lt;/li&gt;
&lt;li&gt;✅ Storage: a bundled PostgreSQL instance for agent state (swap it for your own in production).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The nice part is that the Agent resource does not care where the model lives. It just references a ModelConfig by name. Cloud or local, the agent spec looks the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧰 Prerequisites
&lt;/h2&gt;

&lt;p&gt;Nothing exotic, just homelab basics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ A running Kubernetes cluster (k3s, kind, minikube, or your bare metal cluster all work)&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;kubectl&lt;/code&gt; and &lt;code&gt;helm&lt;/code&gt; installed and pointing at that cluster&lt;/li&gt;
&lt;li&gt;✅ An Anthropic API key for the Claude part (grab one from the Anthropic console)&lt;/li&gt;
&lt;li&gt;✅ Enough CPU and RAM on a node for a local model (an 8B model like &lt;code&gt;llama3.1&lt;/code&gt; is happy with a couple of cores and around 8Gi of memory)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Heads up on versions: everything below was verified against kagent v0.9 (CRD apiVersion &lt;code&gt;kagent.dev/v1alpha2&lt;/code&gt;), current Claude model ids &lt;code&gt;claude-haiku-4-5&lt;/code&gt; and &lt;code&gt;claude-sonnet-4-5&lt;/code&gt;, and the Ollama &lt;code&gt;llama3.1&lt;/code&gt; model, all checked in July 2026. kagent moves fast, so if a field looks different, check the release notes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  📦 Step 1: Install kagent
&lt;/h2&gt;

&lt;p&gt;kagent installs as two Helm charts: one for the CRDs, one for the controller and friends. Since v0.7, the kmcp tool subproject comes bundled, so you get the built in Kubernetes tools out of the box.&lt;/p&gt;

&lt;p&gt;First, install the CRDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm &lt;span class="nb"&gt;install &lt;/span&gt;kagent-crds oci://ghcr.io/kagent-dev/kagent/helm/kagent-crds &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; kagent &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--create-namespace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now install kagent itself. We will set Anthropic as the default provider so we get a working Claude backed model config the moment the pods come up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-anthropic-key-here"&lt;/span&gt;

helm &lt;span class="nb"&gt;install &lt;/span&gt;kagent oci://ghcr.io/kagent-dev/kagent/helm/kagent &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; kagent &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; providers.default&lt;span class="o"&gt;=&lt;/span&gt;anthropic &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; providers.anthropic.apiKey&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$ANTHROPIC_API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give it a minute, then check the pods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; kagent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the controller, the UI, the kagent tool server, and a bundled PostgreSQL pod.&lt;/p&gt;

&lt;p&gt;Want the dashboard? Port forward the UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl port-forward &lt;span class="nt"&gt;-n&lt;/span&gt; kagent svc/kagent-ui 8080:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;a href="http://localhost:8080" rel="noopener noreferrer"&gt;http://localhost:8080&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Homelab tip: the default install ships a bundled &lt;code&gt;postgres:18&lt;/code&gt; pod so you can play right away. For anything you actually care about, point kagent at your own PostgreSQL instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  ☁️ Step 2: Wire up Claude for cloud inference
&lt;/h2&gt;

&lt;p&gt;Because we passed the Anthropic provider at install time, kagent already created a default ModelConfig for us. In v0.9 that default uses &lt;code&gt;claude-haiku-4-5&lt;/code&gt;, which is fast and cheap and perfect for quick cluster questions.&lt;/p&gt;

&lt;p&gt;Let us look at what landed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get modelconfigs &lt;span class="nt"&gt;-n&lt;/span&gt; kagent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let us be explicit and add our own ModelConfig that pins a stronger model, &lt;code&gt;claude-sonnet-4-5&lt;/code&gt;, for the heavier reasoning tasks. First store the key in a Secret (kagent reads the model key from a Secret you control):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-anthropic-key-here"&lt;/span&gt;

kubectl create secret generic kagent-anthropic &lt;span class="nt"&gt;-n&lt;/span&gt; kagent &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt; &lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$ANTHROPIC_API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create the ModelConfig that references it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent.dev/v1alpha2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ModelConfig&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;claude-sonnet&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Anthropic&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;claude-sonnet-4-5&lt;/span&gt;
  &lt;span class="na"&gt;apiKeySecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent-anthropic&lt;/span&gt;
  &lt;span class="na"&gt;apiKeySecretKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ANTHROPIC_API_KEY&lt;/span&gt;
  &lt;span class="na"&gt;anthropic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; claude-modelconfig.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is your cloud inference layer done. Two model configs now live in the cluster: a fast Haiku default and a Sonnet config for the deeper work.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note on model ids: &lt;code&gt;claude-haiku-4-5&lt;/code&gt; and &lt;code&gt;claude-sonnet-4-5&lt;/code&gt; are convenience aliases that resolve to the latest dated snapshot. If you want a fully pinned version for reproducibility, use the dated form such as &lt;code&gt;claude-haiku-4-5-20251001&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🏠 Step 3: Deploy Ollama for on prem inference
&lt;/h2&gt;

&lt;p&gt;Now the fun part for homelabbers: running the model yourself, no tokens leaving your network.&lt;/p&gt;

&lt;p&gt;We will run Ollama as a normal Deployment, give it a PersistentVolumeClaim so the downloaded model survives restarts (models are big, you do not want to re download on every reschedule), and expose it with a Service.&lt;/p&gt;

&lt;p&gt;Create the namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create ns ollama
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the Ollama stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PersistentVolumeClaim&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-models&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;accessModes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ReadWriteOnce&lt;/span&gt;
  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20Gi&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama/ollama:latest&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
              &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;11434&lt;/span&gt;
              &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
          &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;
              &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;8Gi&lt;/span&gt;
            &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;12Gi&lt;/span&gt;
          &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;models&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/root/.ollama&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;models&lt;/span&gt;
          &lt;span class="na"&gt;persistentVolumeClaim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;claimName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-models&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterIP&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for the pod to be ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; ollama &lt;span class="nt"&gt;-w&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it is running, pull a model. This matters: kagent drives agents through tool calling, so you must use a model that supports function calling. Plain &lt;code&gt;llama3&lt;/code&gt; does not do this reliably, but &lt;code&gt;llama3.1&lt;/code&gt; and newer (or &lt;code&gt;qwen2.5&lt;/code&gt; and above) do. We will use &lt;code&gt;llama3.1&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; ollama &lt;span class="nb"&gt;exec &lt;/span&gt;deploy/ollama &lt;span class="nt"&gt;--&lt;/span&gt; ollama pull llama3.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm it landed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; ollama &lt;span class="nb"&gt;exec &lt;/span&gt;deploy/ollama &lt;span class="nt"&gt;--&lt;/span&gt; ollama list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your local model is now reachable inside the cluster at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://ollama.ollama.svc.cluster.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The Service listens on port 80 and forwards to the container on 11434, so no port number is needed in the URL.)&lt;/p&gt;

&lt;h2&gt;
  
  
  🔌 Step 4: Tell kagent about your local model
&lt;/h2&gt;

&lt;p&gt;Same ModelConfig pattern as Claude, just a different provider and a &lt;code&gt;host&lt;/code&gt; that points at the in cluster Ollama Service.&lt;/p&gt;

&lt;p&gt;One small quirk: Ollama does not need an API key, but the ModelConfig field still expects a secret reference. So we create a throwaway placeholder secret (Ollama ignores the value):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create secret generic kagent-ollama &lt;span class="nt"&gt;-n&lt;/span&gt; kagent &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt; &lt;span class="nv"&gt;OLLAMA_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ollama-placeholder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the ModelConfig:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent.dev/v1alpha2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ModelConfig&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-llama31&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ollama&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;llama3.1&lt;/span&gt;
  &lt;span class="na"&gt;apiKeySecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent-ollama&lt;/span&gt;
  &lt;span class="na"&gt;apiKeySecretKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OLLAMA_API_KEY&lt;/span&gt;
  &lt;span class="na"&gt;ollama&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://ollama.ollama.svc.cluster.local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; ollama-modelconfig.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that both cloud and local configs are now registered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get modelconfigs &lt;span class="nt"&gt;-n&lt;/span&gt; kagent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You now have a hybrid inference stack: Claude for the heavy lifting, Ollama for private, offline friendly work, both described the same declarative way.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 Step 5: Build an agent that uses your models
&lt;/h2&gt;

&lt;p&gt;Here is where the abstraction pays off. An Agent is just a system prompt, a set of tools, and a &lt;code&gt;modelConfig&lt;/code&gt; reference. kagent ships a built in tool server called &lt;code&gt;kagent-tool-server&lt;/code&gt; that exposes Kubernetes actions as tools, so our agent can actually inspect the cluster.&lt;/p&gt;

&lt;p&gt;Let us create a Claude backed cluster helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent.dev/v1alpha2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Agent&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster-helper-claude&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A friendly Kubernetes helper backed by Claude.&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Declarative&lt;/span&gt;
  &lt;span class="na"&gt;declarative&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;modelConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;claude-sonnet&lt;/span&gt;
    &lt;span class="na"&gt;systemMessage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;You are a friendly and careful Kubernetes helper.&lt;/span&gt;

      &lt;span class="s"&gt;# Instructions&lt;/span&gt;
      &lt;span class="s"&gt;- Use the available tools to answer questions about the cluster.&lt;/span&gt;
      &lt;span class="s"&gt;- If a question is unclear, ask for clarification before running any tool.&lt;/span&gt;
      &lt;span class="s"&gt;- Never invent an answer. If you are unsure, say so.&lt;/span&gt;

      &lt;span class="s"&gt;# Response format&lt;/span&gt;
      &lt;span class="s"&gt;- Always format your response as Markdown.&lt;/span&gt;
      &lt;span class="s"&gt;- Summarize the actions you took and explain the result.&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;McpServer&lt;/span&gt;
        &lt;span class="na"&gt;mcpServer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent-tool-server&lt;/span&gt;
          &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RemoteMCPServer&lt;/span&gt;
          &lt;span class="na"&gt;apiGroup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent.dev&lt;/span&gt;
          &lt;span class="na"&gt;toolNames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;k8s_get_resources&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;k8s_get_available_api_resources&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;k8s_describe_resource&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; agent-claude.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 Now the magic trick. To run the exact same agent on your local model instead of Claude, you change one field: &lt;code&gt;modelConfig&lt;/code&gt;. Here is the on prem twin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent.dev/v1alpha2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Agent&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster-helper-local&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The same helper, running fully local on Ollama.&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Declarative&lt;/span&gt;
  &lt;span class="na"&gt;declarative&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;modelConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-llama31&lt;/span&gt;
    &lt;span class="na"&gt;systemMessage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;You are a friendly and careful Kubernetes helper.&lt;/span&gt;

      &lt;span class="s"&gt;# Instructions&lt;/span&gt;
      &lt;span class="s"&gt;- Use the available tools to answer questions about the cluster.&lt;/span&gt;
      &lt;span class="s"&gt;- If a question is unclear, ask for clarification before running any tool.&lt;/span&gt;
      &lt;span class="s"&gt;- Never invent an answer. If you are unsure, say so.&lt;/span&gt;

      &lt;span class="s"&gt;# Response format&lt;/span&gt;
      &lt;span class="s"&gt;- Always format your response as Markdown.&lt;/span&gt;
      &lt;span class="s"&gt;- Summarize the actions you took and explain the result.&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;McpServer&lt;/span&gt;
        &lt;span class="na"&gt;mcpServer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent-tool-server&lt;/span&gt;
          &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RemoteMCPServer&lt;/span&gt;
          &lt;span class="na"&gt;apiGroup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent.dev&lt;/span&gt;
          &lt;span class="na"&gt;toolNames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;k8s_get_resources&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;k8s_get_available_api_resources&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;k8s_describe_resource&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; agent-local.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same prompt, same tools, different brain. That is the whole point of putting a ModelConfig in front of your agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  💬 Step 6: Talk to your agents
&lt;/h2&gt;

&lt;p&gt;You can chat through the dashboard, but the CLI is quicker for a test. Grab the kagent CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;kagent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://raw.githubusercontent.com/kagent-dev/kagent/refs/heads/main/scripts/get-kagent | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;List your agents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kagent get agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask the Claude agent something:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kagent invoke &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"What API resources are available in my cluster?"&lt;/span&gt; &lt;span class="nt"&gt;--agent&lt;/span&gt; cluster-helper-claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask the local one the same thing and compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kagent invoke &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"List the pods in the kagent namespace and tell me if any are unhealthy."&lt;/span&gt; &lt;span class="nt"&gt;--agent&lt;/span&gt; cluster-helper-local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The local agent keeps every token inside your homelab, which is a lovely thing when you are poking at private workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛡️ Step 7: Add a safety gate (highly recommended)
&lt;/h2&gt;

&lt;p&gt;Giving an AI agent access to your cluster is great until it decides to be too helpful. kagent has a built in Human in the Loop feature: mark any tool as needing approval, and the agent pauses for your yes or no before running it.&lt;/p&gt;

&lt;p&gt;Here is an agent that can read freely but must ask before it changes anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent.dev/v1alpha2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Agent&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster-operator&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A Kubernetes operator agent with approval gates on destructive actions.&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Declarative&lt;/span&gt;
  &lt;span class="na"&gt;declarative&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;modelConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;claude-sonnet&lt;/span&gt;
    &lt;span class="na"&gt;systemMessage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;You are a Kubernetes operator assistant.&lt;/span&gt;
      &lt;span class="s"&gt;Before making any change, explain what you plan to do and why.&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;McpServer&lt;/span&gt;
        &lt;span class="na"&gt;mcpServer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent-tool-server&lt;/span&gt;
          &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RemoteMCPServer&lt;/span&gt;
          &lt;span class="na"&gt;apiGroup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent.dev&lt;/span&gt;
          &lt;span class="na"&gt;toolNames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;k8s_get_resources&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;k8s_describe_resource&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;k8s_apply_manifest&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;k8s_delete_resource&lt;/span&gt;
          &lt;span class="na"&gt;requireApproval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;k8s_apply_manifest&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;k8s_delete_resource&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; agent-operator.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the read tools run freely, but the moment the agent tries to apply or delete something, the UI shows Approve and Reject buttons. If you reject, your reason is fed back to the model as context. Two lines of YAML, a lot of peace of mind.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Good to know: every tool you list in &lt;code&gt;requireApproval&lt;/code&gt; must also appear in &lt;code&gt;toolNames&lt;/code&gt;, and kagent validates this when the resource is created.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🔁 Why this pattern is nice for a homelab
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ One workflow: agents, models, and tools are all just &lt;code&gt;kubectl apply&lt;/code&gt;, so GitOps and PR reviews work exactly like the rest of your cluster.&lt;/li&gt;
&lt;li&gt;✅ No lock in: swap Claude for Ollama, or route different agents to different models, without rewriting anything.&lt;/li&gt;
&lt;li&gt;✅ Cost control: send routine questions to a local model, save the cloud model for the hard stuff.&lt;/li&gt;
&lt;li&gt;✅ Privacy by default: keep sensitive workloads on the on prem model, no data leaves the house.&lt;/li&gt;
&lt;li&gt;✅ Guardrails built in: approval gates and clear observability instead of a black box.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🚀 What is next
&lt;/h2&gt;

&lt;p&gt;A few directions to keep exploring from here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Point kagent at an external PostgreSQL and enable memory so your agents remember context across sessions.&lt;/li&gt;
&lt;li&gt;✅ Try the Go runtime for faster agent cold starts by setting &lt;code&gt;runtime: go&lt;/code&gt; in the declarative spec.&lt;/li&gt;
&lt;li&gt;✅ Add your own MCP tools with kmcp so agents can talk to your internal APIs.&lt;/li&gt;
&lt;li&gt;✅ Put a GPU node under Ollama and jump to a bigger local model for better tool use.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🦆 Bonus: drive it all from Goose
&lt;/h2&gt;

&lt;p&gt;Once your stack is up, you do not have to live inside the kagent UI. Goose is a local, open source AI agent (CLI and desktop, now part of the Linux Foundation Agentic AI Foundation) that speaks MCP. That means it can plug straight into what you just built, in two complementary ways.&lt;/p&gt;

&lt;p&gt;Think of it like this: Goose needs a brain (a model provider) and it can borrow tools (MCP extensions). Your cluster can supply both.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔑 First, give Goose a brain (your deployed models)
&lt;/h3&gt;

&lt;p&gt;Install Goose from the official install page (&lt;a href="https://goose-docs.ai/docs/getting-started/installation" rel="noopener noreferrer"&gt;https://goose-docs.ai/docs/getting-started/installation&lt;/a&gt;), then run the config wizard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;goose configure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point it at Claude, using the same Anthropic key you gave kagent. Goose will ask for your API key and store it in your system keyring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;┌   goose-configure
│
◇  What would you like to configure?
│  Configure Providers
│
◇  Which model provider should we use?
│  Anthropic
│
◇  Enter a model from that provider:
│  claude-sonnet-4-5
└  Configuration saved successfully
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer to stay fully local and reuse the Ollama you deployed in Step 3? Goose runs on your workstation, so first port forward the in cluster Ollama Service (it listens on port 80 and targets the container on 11434):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; ollama port-forward svc/ollama 11434:80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then configure Goose to use Ollama. If you do not set a host, Goose defaults to localhost:11434, which is exactly where the port forward lands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;┌   goose-configure
│
◇  What would you like to configure?
│  Configure Providers
│
◇  Which model provider should we use?
│  Ollama
│
◇  Enter a model from that provider:
│  llama3.1
└  Configuration saved successfully
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 One thing to keep in mind: Goose leans heavily on tool calling, so pick a model that is good at it. llama3.1 and up or qwen2.5 handle light tasks, but for serious agent loops Claude will be a lot more reliable.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔗 Then, hand Goose your kagent agents as tools
&lt;/h3&gt;

&lt;p&gt;kagent exposes every running agent through an MCP server built into the control plane, over the Streamable HTTP transport. So Goose can discover and call your kagent agents as if they were tools.&lt;/p&gt;

&lt;p&gt;Port forward the kagent control plane:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl port-forward &lt;span class="nt"&gt;-n&lt;/span&gt; kagent svc/kagent-controller 8083:8083
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it to Goose as a remote extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;goose configure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;┌   goose-configure
│
◇  What would you like to configure?
│  Add Extension
│
◇  What type of extension would you like to add?
│  Remote Extension (Streamable HTTP)
│
◇  What would you like to call this extension?
│  kagent-agents
│
◇  What is the Streamable HTTP endpoint URI?
│  http://localhost:8083/mcp
│
◇  Please set the timeout for this tool (in secs):
│  300
└  Added kagent-agents extension
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That writes an entry to &lt;code&gt;~/.config/goose/config.yaml&lt;/code&gt; that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;extensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kagent-agents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;streamable_http&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kagent-agents&lt;/span&gt;
    &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8083/mcp&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The kagent MCP server hands Goose two tools: &lt;code&gt;list_agents&lt;/code&gt; to discover what is available, and &lt;code&gt;invoke_agent&lt;/code&gt; to run a specific agent by name (with session support for follow ups). Start a session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;goose session
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and ask something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;List my kagent agents, then use the cluster-helper-local agent to check for unhealthy pods in the kagent namespace.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Goose will call &lt;code&gt;list_agents&lt;/code&gt;, pick &lt;code&gt;cluster-helper-local&lt;/code&gt;, and delegate the cluster work to it through kagent. You end up with Goose as the friendly front door, your kagent agents as specialized workers, and Claude or Ollama as the brain, all wired together through open protocols.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: this kagent endpoint currently supports Streamable HTTP, not SSE. If you put kagent behind a gateway you can skip the port forward and point Goose at the public URL instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🎁 Wrapping Up
&lt;/h2&gt;

&lt;p&gt;We went from an empty homelab cluster to a working, hybrid AI inference stack: kagent as the control plane, Claude for cloud inference, and Ollama running fully local, all described as plain Kubernetes resources. The best part is how boring it feels in the best way. Agents are just workloads, models are just config, and swapping between cloud and on prem is a one line change.&lt;/p&gt;

&lt;p&gt;If you have been waiting for a reason to bring agentic AI into your cluster without handing everything to a SaaS, this is a really pleasant place to start.&lt;/p&gt;

&lt;p&gt;Happy clustering and stay safe! 🧑‍🚀&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cloud</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>🚦 Meet Kueue: Smart Job Queueing for Kubernetes 🧠⚙️</title>
      <dc:creator>Hamdi (KHELIL) LION</dc:creator>
      <pubDate>Tue, 30 Jun 2026 06:32:30 +0000</pubDate>
      <link>https://dev.to/hkhelil/meet-kueue-smart-job-queueing-for-kubernetes-3gj</link>
      <guid>https://dev.to/hkhelil/meet-kueue-smart-job-queueing-for-kubernetes-3gj</guid>
      <description>&lt;p&gt;Hey everyone 👋&lt;/p&gt;

&lt;p&gt;If you run batch jobs, data pipelines, or any kind of AI and ML training on Kubernetes, you have probably hit this wall. Kubernetes is fantastic at deciding WHERE a pod should run, but it is surprisingly clueless about WHEN a job should start. 😅&lt;/p&gt;

&lt;p&gt;You submit ten jobs, the cluster fills up, and the rest just sit there as Pending. No real queue, no priority, no fairness between teams. One noisy team can eat all your expensive nodes while everyone else waits. 🥲&lt;/p&gt;

&lt;p&gt;That is exactly the gap Kueue fills, and today I want to walk you through it with a pile of hands on examples you can run on any cluster, even your homelab. 🏡&lt;/p&gt;

&lt;p&gt;👉 Key takeaway up front: Kueue is a job level manager that holds your jobs in a real queue and only admits them when there is enough quota to actually run them.&lt;/p&gt;

&lt;p&gt;🧪 Everything in this guide was tested against Kueue v0.18.1 using the v1beta2 API. I pinned every command and manifest to that version so you do not get surprised by API drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  📋 What we will cover
&lt;/h2&gt;

&lt;p&gt;✅ Why Kubernetes needs a queue&lt;br&gt;
✅ The building blocks in plain language&lt;br&gt;
✅ Installing Kueue&lt;br&gt;
✅ Setting up quota with a ResourceFlavor, a ClusterQueue, and a LocalQueue&lt;br&gt;
✅ Submitting a Job and watching it get queued and admitted&lt;br&gt;
✅ Priority based admission&lt;br&gt;
✅ Partial admission and elastic jobs&lt;br&gt;
✅ Multiple resource flavors for x86 and arm&lt;br&gt;
✅ Fair sharing between teams with cohorts&lt;br&gt;
✅ Dedicated quota with a shared fallback&lt;br&gt;
✅ Queueing a plain Pod&lt;br&gt;
✅ Why this matters a lot for GPUs and your cloud bill&lt;/p&gt;
&lt;h2&gt;
  
  
  🤔 Why Kubernetes needs a queue
&lt;/h2&gt;

&lt;p&gt;Native Kubernetes scheduling is pod centric. The scheduler looks at one pod at a time and tries to place it. That works great for long running services.&lt;/p&gt;

&lt;p&gt;Batch workloads are different. They have a beginning and an end, they often need a fixed chunk of capacity, and they compete with other teams for the same nodes.&lt;/p&gt;

&lt;p&gt;Without a queueing layer you get:&lt;/p&gt;

&lt;p&gt;✅ Jobs that fail or stay Pending when resources are tight&lt;br&gt;
✅ No quota governance, so one team can starve the others&lt;br&gt;
✅ No admission priority, so a quick experiment can block production training&lt;/p&gt;
&lt;h2&gt;
  
  
  🧠 What is Kueue
&lt;/h2&gt;

&lt;p&gt;Kueue is a Kubernetes native job queueing system, maintained as a kubernetes-sigs project. It does not replace the scheduler. It sits in front of it. 🛂&lt;/p&gt;

&lt;p&gt;Here is the simple mental model. Think of the Kubernetes scheduler as the runway, and Kueue as the control tower deciding which flight is cleared for takeoff and when. ✈️&lt;/p&gt;

&lt;p&gt;When a job arrives, Kueue suspends it, creates a matching Workload object, checks if there is enough quota, and only then lets the pods be created. If there is no room, the job waits politely in the queue instead of failing.&lt;/p&gt;
&lt;h2&gt;
  
  
  🧩 The building blocks
&lt;/h2&gt;

&lt;p&gt;There are four pieces you need to know, plus one bonus piece for teams.&lt;/p&gt;

&lt;p&gt;✅ ResourceFlavor 🍦&lt;br&gt;
Describes a type of resource, usually tied to node labels. For example x86 nodes versus arm nodes, or GPU nodes versus CPU nodes. If you do not need to distinguish node types, you use one empty flavor.&lt;/p&gt;

&lt;p&gt;✅ ClusterQueue 🏦&lt;br&gt;
A cluster scoped object that holds the actual quota. This is where you say how much cpu, memory, or how many GPUs are available. Users do not submit to it directly.&lt;/p&gt;

&lt;p&gt;✅ LocalQueue 📥&lt;br&gt;
A namespaced object that points to a ClusterQueue. This is what users actually target with their jobs.&lt;/p&gt;

&lt;p&gt;✅ Workload 📦&lt;br&gt;
The internal object Kueue creates for each job to track its admission state. You usually just observe it.&lt;/p&gt;

&lt;p&gt;✅ Cohort 👥 (bonus)&lt;br&gt;
A group of ClusterQueues that can borrow each other unused quota. This is the magic behind fair sharing between teams.&lt;/p&gt;
&lt;h2&gt;
  
  
  🛠️ Step 1: Install Kueue
&lt;/h2&gt;

&lt;p&gt;The simplest method is to apply the released manifests with server side apply.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;--server-side&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; https://github.com/kubernetes-sigs/kueue/releases/download/v0.18.1/manifests.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The controller runs in the kueue-system namespace. Give it a few seconds and check it is healthy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get deploy &lt;span class="nt"&gt;-n&lt;/span&gt; kueue-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the controller manager become ready.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
kueue-controller-manager   1/1     1            1           30s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer Helm? Kueue publishes an OCI chart for each release. Just make sure the chart version matches the release you want.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm &lt;span class="nb"&gt;install &lt;/span&gt;kueue oci://registry.k8s.io/kueue/charts/kueue &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.18.1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; kueue-system &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--create-namespace&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--wait&lt;/span&gt; &lt;span class="nt"&gt;--timeout&lt;/span&gt; 300s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🍦 Step 2: Create a ResourceFlavor
&lt;/h2&gt;

&lt;p&gt;Since we are not distinguishing node types in this first demo, an empty flavor is all we need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# default-flavor.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kueue.x-k8s.io/v1beta2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ResourceFlavor&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default-flavor"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; default-flavor.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🏦 Step 3: Create a ClusterQueue
&lt;/h2&gt;

&lt;p&gt;Now we define the quota for the whole cluster. Here we allow 9 cpu and 36Gi of memory, all served by our single flavor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# cluster-queue.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kueue.x-k8s.io/v1beta2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterQueue&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cluster-queue"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;namespaceSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt; &lt;span class="c1"&gt;# match all namespaces&lt;/span&gt;
  &lt;span class="na"&gt;resourceGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;coveredResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;flavors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default-flavor"&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu"&lt;/span&gt;
        &lt;span class="na"&gt;nominalQuota&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory"&lt;/span&gt;
        &lt;span class="na"&gt;nominalQuota&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;36Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; cluster-queue.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One important detail. The flavor name under spec.resourceGroups must match the ResourceFlavor name from step 2. If they do not match, the ClusterQueue will not become ready. 🔗&lt;/p&gt;

&lt;h2&gt;
  
  
  📥 Step 4: Create a LocalQueue
&lt;/h2&gt;

&lt;p&gt;Users cannot send work to a ClusterQueue directly. They need a LocalQueue in their namespace that points to it. We will put ours in the default namespace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# default-user-queue.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kueue.x-k8s.io/v1beta2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LocalQueue&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default"&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user-queue"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;clusterQueue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cluster-queue"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; default-user-queue.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick tip: you can apply all three of the above at once using the example bundle from the project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; https://kueue.sigs.k8s.io/examples/admin/single-clusterqueue-setup.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚀 Step 5: Submit your first Job
&lt;/h2&gt;

&lt;p&gt;This is the only change your users need to make to an existing Job. Add the kueue.x-k8s.io/queue-name label pointing to the LocalQueue, and make sure each pod declares resource requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# sample-job.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Job&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generateName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sample-job-&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kueue.x-k8s.io/queue-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user-queue&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;parallelism&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;completions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dummy-job&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.k8s.io/e2e-test-images/agnhost:2.53&lt;/span&gt;
        &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/bin/sh"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sleep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;60"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
            &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;200Mi"&lt;/span&gt;
      &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Never&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that you do not need to set the job to suspended yourself. Kueue manages suspension for you through a webhook and decides the best moment to start it. 🪄&lt;/p&gt;

&lt;p&gt;Create the job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create &lt;span class="nt"&gt;-f&lt;/span&gt; sample-job.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔭 Step 6: Watch the queue work
&lt;/h2&gt;

&lt;p&gt;List your local queues. The alias queues also works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; default get localqueues
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME         CLUSTERQUEUE    PENDING WORKLOADS
user-queue   cluster-queue   0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kueue creates a Workload object for your job. Have a look.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; default get workloads.kueue.x-k8s.io
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME               QUEUE         RESERVED IN     ADMITTED   AGE
sample-job-xxxxx   user-queue    cluster-queue   True       3s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want the full story? Describe the workload. When there is not enough quota, you will see it sit unadmitted with a clear message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; default describe workload sample-job-xxxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Status:
  Conditions:
    Message:  workload didn't fit
    Reason:   Pending
    Status:   False
    Type:     Admitted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The moment quota frees up, Kueue admits it automatically. If you describe the Job itself, the event timeline tells the whole story.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Events:
  Type    Reason            From                  Message
  ----    ------            ----                  -------
  Normal  Suspended         job-controller        Job suspended
  Normal  CreatedWorkload   kueue-job-controller  Created Workload: default/sample-job-xxxxx
  Normal  Started           kueue-job-controller  Admitted by clusterQueue cluster-queue
  Normal  Resumed           job-controller        Job resumed
  Normal  Completed         job-controller        Job completed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No babysitting required. 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  🥇 Example: priority based admission
&lt;/h2&gt;

&lt;p&gt;Inside a queue, not all jobs are equal. With a WorkloadPriorityClass you can control admission and preemption priority independently from pod priority. Production training jumps the line ahead of throwaway experiments. 🏎️&lt;/p&gt;

&lt;p&gt;First create the priority class.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# sample-priority.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kueue.x-k8s.io/v1beta2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WorkloadPriorityClass&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sample-priority&lt;/span&gt;
&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sample&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;priority"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point a Job at it with the kueue.x-k8s.io/priority-class label.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# priority-job.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Job&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sample-job&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kueue.x-k8s.io/queue-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user-queue&lt;/span&gt;
    &lt;span class="na"&gt;kueue.x-k8s.io/priority-class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sample-priority&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;parallelism&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;completions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;suspend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dummy-job&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.k8s.io/e2e-test-images/agnhost:latest&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pause"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Never&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Higher value means higher priority for queuing and preemption. The neat part is this priority does not touch the pod priority, so it does not interfere with your normal Kubernetes scheduling. 👌&lt;/p&gt;

&lt;h2&gt;
  
  
  ✂️ Example: partial admission
&lt;/h2&gt;

&lt;p&gt;Sometimes a big job can still make progress with fewer pods. With the kueue.x-k8s.io/job-min-parallelism annotation, Kueue can admit the job at a reduced parallelism instead of leaving it Pending.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# partial-job.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Job&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sample-job-partial-admission&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kueue.x-k8s.io/queue-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user-queue&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kueue.x-k8s.io/job-min-parallelism&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;parallelism&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
  &lt;span class="na"&gt;completions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dummy-job&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.k8s.io/e2e-test-images/agnhost:2.53&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entrypoint-tester"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;world"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
            &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;200Mi"&lt;/span&gt;
      &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Never&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If only 9 cpu is free, this job is admitted with parallelism 9 instead of waiting for all 20. The completions count stays the same. 🙌&lt;/p&gt;

&lt;h2&gt;
  
  
  📈 Example: elastic jobs
&lt;/h2&gt;

&lt;p&gt;Elastic jobs let you change a running Job parallelism without recreating, restarting, or suspending it. This is an alpha feature, so you must enable the ElasticJobsViaWorkloadSlices feature gate and annotate the Job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# elastic-job.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Job&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sample-elastic-job&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kueue.x-k8s.io/elastic-job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kueue.x-k8s.io/queue-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user-queue&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;parallelism&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;completions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dummy-job&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.k8s.io/e2e-test-images/agnhost:2.53&lt;/span&gt;
        &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/bin/sh"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sleep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;60"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;100m"&lt;/span&gt;
            &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;100Mi"&lt;/span&gt;
      &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Never&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you bump parallelism up, Kueue creates a new admitted Workload for the new pod count and marks the old one as Finished. When you scale down, the extra pods terminate and no new Workload is created. Smooth. 🧘&lt;/p&gt;

&lt;h2&gt;
  
  
  🧱 Example: multiple resource flavors
&lt;/h2&gt;

&lt;p&gt;Real clusters often mix node types. Say you have x86 and arm nodes labelled with cpu-arch. You can create one flavor per architecture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# flavor-x86.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kueue.x-k8s.io/v1beta2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ResourceFlavor&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x86"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nodeLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu-arch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;x86&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# flavor-arm.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kueue.x-k8s.io/v1beta2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ResourceFlavor&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arm"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nodeLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu-arch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reference both in a single ClusterQueue. Here cpu is split across the two architectures, while memory uses the simple default flavor because we do not care which architecture provides it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# cluster-queue-multi.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kueue.x-k8s.io/v1beta2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterQueue&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cluster-queue"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;namespaceSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt; &lt;span class="c1"&gt;# match all&lt;/span&gt;
  &lt;span class="na"&gt;resourceGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;coveredResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;flavors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x86"&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu"&lt;/span&gt;
        &lt;span class="na"&gt;nominalQuota&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arm"&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu"&lt;/span&gt;
        &lt;span class="na"&gt;nominalQuota&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;coveredResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;flavors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default-flavor"&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory"&lt;/span&gt;
        &lt;span class="na"&gt;nominalQuota&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;84Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The labels in the ResourceFlavor must match the labels on your nodes. If you use the cluster autoscaler, make sure it adds those labels to new nodes too. 🏷️&lt;/p&gt;

&lt;h2&gt;
  
  
  👥 Example: fair sharing between teams
&lt;/h2&gt;

&lt;p&gt;This is where Kueue really shines. Put two ClusterQueues in the same cohort and they can borrow each other unused quota.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# team-a-cq.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kueue.x-k8s.io/v1beta2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterQueue&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;team-a-cq"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;namespaceSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
  &lt;span class="na"&gt;cohortName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;team-ab"&lt;/span&gt;
  &lt;span class="na"&gt;resourceGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;coveredResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;flavors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default-flavor"&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu"&lt;/span&gt;
        &lt;span class="na"&gt;nominalQuota&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9&lt;/span&gt;
        &lt;span class="na"&gt;borrowingLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory"&lt;/span&gt;
        &lt;span class="na"&gt;nominalQuota&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;36Gi&lt;/span&gt;
        &lt;span class="na"&gt;borrowingLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;24Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# team-b-cq.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kueue.x-k8s.io/v1beta2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterQueue&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;team-b-cq"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;namespaceSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
  &lt;span class="na"&gt;cohortName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;team-ab"&lt;/span&gt;
  &lt;span class="na"&gt;resourceGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;coveredResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;flavors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default-flavor"&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu"&lt;/span&gt;
        &lt;span class="na"&gt;nominalQuota&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory"&lt;/span&gt;
        &lt;span class="na"&gt;nominalQuota&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;48Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both queues belong to the cohort team-ab. Team A has its own guaranteed quota, but it can also borrow idle capacity from Team B, up to the borrowingLimit of 6 cpu and 24Gi. When Team B needs its capacity back, Kueue handles it. ⚖️&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 Example: dedicated quota with a shared fallback
&lt;/h2&gt;

&lt;p&gt;A ClusterQueue can borrow from the cohort even when it has zero nominal quota for a flavor. This lets you give each team dedicated capacity on one flavor, plus a shared pool to fall back on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# team-a-cq.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kueue.x-k8s.io/v1beta2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterQueue&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;team-a-cq"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;namespaceSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt; &lt;span class="c1"&gt;# match all&lt;/span&gt;
  &lt;span class="na"&gt;cohortName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;team-ab"&lt;/span&gt;
  &lt;span class="na"&gt;resourceGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;coveredResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;flavors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arm"&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu"&lt;/span&gt;
        &lt;span class="na"&gt;nominalQuota&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9&lt;/span&gt;
        &lt;span class="na"&gt;borrowingLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x86"&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu"&lt;/span&gt;
        &lt;span class="na"&gt;nominalQuota&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;coveredResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;flavors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default-flavor"&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory"&lt;/span&gt;
        &lt;span class="na"&gt;nominalQuota&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;36Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# shared-cq.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kueue.x-k8s.io/v1beta2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterQueue&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared-cq"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;namespaceSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt; &lt;span class="c1"&gt;# match all&lt;/span&gt;
  &lt;span class="na"&gt;cohortName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;team-ab"&lt;/span&gt;
  &lt;span class="na"&gt;resourceGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;coveredResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;flavors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x86"&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu"&lt;/span&gt;
        &lt;span class="na"&gt;nominalQuota&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;coveredResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;flavors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default-flavor"&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory"&lt;/span&gt;
        &lt;span class="na"&gt;nominalQuota&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;24Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it like this:&lt;/p&gt;

&lt;p&gt;✅ team-a-cq has a borrowingLimit of 0 on the arm flavor, so its arm capacity is truly dedicated and cannot be borrowed away.&lt;br&gt;
✅ team-a-cq has a nominalQuota of 0 on the x86 flavor, so it has no x86 of its own and can only borrow x86 from shared-cq.&lt;/p&gt;

&lt;p&gt;This pattern is great for giving each team a guaranteed slice while still pooling the expensive shared hardware. 🤝&lt;/p&gt;
&lt;h2&gt;
  
  
  🫙 Example: queue a plain Pod
&lt;/h2&gt;

&lt;p&gt;You are not limited to Jobs. Kueue can manage plain Pods too. Just add the queue-name label and resource requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# kueue-sleep-pod.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generateName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kueue-sleep-&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kueue.x-k8s.io/queue-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user-queue&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sleep&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;busybox&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sleep&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OnFailure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kueue injects a kueue.x-k8s.io/managed=true label to mark the pods it manages. The same label driven approach works for Deployments, StatefulSets, RayJobs, JobSets, and Kubeflow jobs as well. 🧰&lt;/p&gt;

&lt;h2&gt;
  
  
  🧭 Bonus: the kueue kubectl plugin
&lt;/h2&gt;

&lt;p&gt;Kueue ships a kubectl plugin so you can manage queues without writing kubectl get workloads.kueue.x-k8s.io every time. Once installed, you get handy commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List workloads in a namespace&lt;/span&gt;
kubectl kueue list workload

&lt;span class="c"&gt;# Stop a workload (it stays in the queue but will not be admitted)&lt;/span&gt;
kubectl kueue stop workload sample-job-xxxxx

&lt;span class="c"&gt;# Resume it later&lt;/span&gt;
kubectl kueue resume workload sample-job-xxxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also covers create, delete, describe, edit, get, and patch for clusterqueues, localqueues, resourceflavors, and workloads. A nice quality of life upgrade for operators. 🧑‍🔧&lt;/p&gt;

&lt;h2&gt;
  
  
  💰 Why this is a big deal for GPUs and FinOps
&lt;/h2&gt;

&lt;p&gt;Here is the part that makes finance happy. 🤑&lt;/p&gt;

&lt;p&gt;GPU and accelerator nodes are expensive, and they are often the scarcest resource in the cluster. The worst outcome is a job that partially grabs a few GPUs, then waits forever for the rest while those GPUs sit idle and billed.&lt;/p&gt;

&lt;p&gt;With Kueue you get:&lt;/p&gt;

&lt;p&gt;✅ Quota governance so no single team hoards the accelerators&lt;br&gt;
✅ Admission only when the capacity a job needs is available&lt;br&gt;
✅ Priority so production training is admitted before throwaway experiments&lt;br&gt;
✅ Borrowing so idle quota is actually used instead of wasted&lt;/p&gt;

&lt;p&gt;That combination is exactly why Kueue is becoming a key building block for running AI and ML workloads on Kubernetes at scale. 🚀&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚠️ A few gotchas to save you time
&lt;/h2&gt;

&lt;p&gt;✅ Always set resource requests on your pods. If you only set limits, Kueue treats the limits as requests. If you set neither, quota accounting cannot work.&lt;br&gt;
✅ The queue-name label must point to a LocalQueue that exists in the same namespace as the job.&lt;br&gt;
✅ The flavor names in the ClusterQueue must match your ResourceFlavor names exactly.&lt;br&gt;
✅ Elastic jobs are alpha, so remember to enable the ElasticJobsViaWorkloadSlices feature gate.&lt;br&gt;
✅ Stick to one API version. This guide uses v1beta2, which is the current served version in v0.18.1.&lt;/p&gt;

&lt;h2&gt;
  
  
  🏁 Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Kueue takes Kubernetes from I will place pods wherever and whenever to I will admit jobs in a fair, prioritized, quota aware order. For batch, data, and AI workloads that is a huge upgrade, and it costs you almost nothing to adopt since your jobs only need one extra label. 🙌&lt;/p&gt;

&lt;p&gt;To recap the flow:&lt;/p&gt;

&lt;p&gt;✅ Install Kueue&lt;br&gt;
✅ Create a ResourceFlavor&lt;br&gt;
✅ Create a ClusterQueue with your quota&lt;br&gt;
✅ Create a LocalQueue per namespace&lt;br&gt;
✅ Add the queue-name label to your jobs&lt;br&gt;
✅ Layer on priority, partial admission, elastic scaling, and cohorts as you grow&lt;/p&gt;

&lt;p&gt;Give it a spin on a small cluster first, watch the Workload objects, and you will quickly get a feel for how admission works.&lt;/p&gt;

&lt;p&gt;What is next? I am going to bring an AI agent into my own homelab cluster and show the full setup, so stay tuned for that one. 🤖🏡&lt;/p&gt;

&lt;p&gt;Happy queueing and stay safe! 👋&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>opensource</category>
      <category>cloud</category>
    </item>
    <item>
      <title>🧹 Keeping Your Kubernetes Cluster Clean with the Descheduler</title>
      <dc:creator>Hamdi (KHELIL) LION</dc:creator>
      <pubDate>Fri, 26 Jun 2026 08:51:59 +0000</pubDate>
      <link>https://dev.to/hkhelil/keeping-your-kubernetes-cluster-clean-with-the-descheduler-41e5</link>
      <guid>https://dev.to/hkhelil/keeping-your-kubernetes-cluster-clean-with-the-descheduler-41e5</guid>
      <description>&lt;p&gt;Your scheduler did a great job placing pods this morning. But your cluster never stops moving, and by this afternoon those decisions are already a little bit wrong. 😅&lt;/p&gt;

&lt;p&gt;The kube-scheduler only decides things once, at pod creation time. After that it walks away. The &lt;strong&gt;descheduler&lt;/strong&gt; is the friend that comes back later, looks at the mess, and gently tidies up.&lt;/p&gt;

&lt;p&gt;Let me walk you through it. 🚀&lt;/p&gt;

&lt;h2&gt;
  
  
  🗺️ what we will cover
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🤔 why a perfectly scheduled cluster slowly drifts&lt;/li&gt;
&lt;li&gt;🧹 what the descheduler does (and what it does not do)&lt;/li&gt;
&lt;li&gt;🧩 the building blocks: profiles, plugins and extension points&lt;/li&gt;
&lt;li&gt;🔌 the strategy plugins you will actually reach for&lt;/li&gt;
&lt;li&gt;🛡️ the Default Evictor, your safety net&lt;/li&gt;
&lt;li&gt;🚀 installing it with Helm (full values file)&lt;/li&gt;
&lt;li&gt;⏱️ CronJob vs Deployment mode&lt;/li&gt;
&lt;li&gt;🧪 testing safely with dry run&lt;/li&gt;
&lt;li&gt;🧯 a production safety checklist&lt;/li&gt;
&lt;li&gt;🧠 the gotcha that bites everyone&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🤔 why your cluster drifts
&lt;/h2&gt;

&lt;p&gt;The scheduler makes a one time decision based on the cluster as it looked the moment a pod appeared. Real clusters keep changing under that decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🆕 New nodes join (autoscaler or manual) and sit half empty while old nodes stay packed.&lt;/li&gt;
&lt;li&gt;🔧 You drain and uncordon a node for maintenance, and nothing moves back onto it.&lt;/li&gt;
&lt;li&gt;🏷️ Labels and taints change, so pods that matched the old rules now violate them.&lt;/li&gt;
&lt;li&gt;💥 Nodes fail, pods pile up elsewhere, and the spread never recovers on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is &lt;strong&gt;drift&lt;/strong&gt;: hotspots, lopsided utilization, and affinity rules that are quietly broken.&lt;/p&gt;

&lt;p&gt;👉 The descheduler exists to correct this drift, on a schedule, without you babysitting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧹 what it actually does
&lt;/h2&gt;

&lt;p&gt;Here is the part people get wrong, so let me be blunt about it.&lt;/p&gt;

&lt;p&gt;The descheduler does &lt;strong&gt;not&lt;/strong&gt; schedule pods. It only &lt;strong&gt;evicts&lt;/strong&gt; them. 🙅&lt;/p&gt;

&lt;p&gt;It finds pods that are poorly placed, evicts them through the standard Kubernetes Eviction API, and then trusts the normal kube-scheduler to recreate them in a better spot.&lt;/p&gt;

&lt;p&gt;That has two big consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ It plays nicely with the scheduler you already have, no replacement needed.&lt;/li&gt;
&lt;li&gt;✅ It respects PodDisruptionBudgets, because it uses the same eviction path everything else does. If an eviction would break a PDB, the request is rejected and that pod is skipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the mental model is simple: &lt;strong&gt;the descheduler makes room, the scheduler fills it.&lt;/strong&gt; 🔁&lt;/p&gt;

&lt;h2&gt;
  
  
  🧩 the building blocks
&lt;/h2&gt;

&lt;p&gt;Modern descheduler config uses the &lt;code&gt;descheduler/v1alpha2&lt;/code&gt; API. Three concepts matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Profiles 📋&lt;/strong&gt;&lt;br&gt;
A profile is a named bundle of plugins and their config. You can run more than one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Plugins 🔌&lt;/strong&gt;&lt;br&gt;
Each strategy is a plugin (for example &lt;code&gt;LowNodeUtilization&lt;/code&gt;). You configure it under &lt;code&gt;pluginConfig&lt;/code&gt; and then switch it on under &lt;code&gt;plugins&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Extension points 🪝&lt;/strong&gt;&lt;br&gt;
This is where a plugin runs. The two you care about for strategies are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;deschedule&lt;/code&gt;: looks at pods one by one and evicts the bad ones.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;balance&lt;/code&gt;: looks across nodes and evicts to even things out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are also &lt;code&gt;filter&lt;/code&gt; and &lt;code&gt;preEvictionFilter&lt;/code&gt; points, which the Default Evictor uses to decide what is safe to touch.&lt;/p&gt;

&lt;p&gt;Here is the shape of a policy so the pieces click:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;descheduler/v1alpha2"&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DeschedulerPolicy"&lt;/span&gt;
&lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
    &lt;span class="na"&gt;pluginConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LowNodeUtilization"&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;thresholds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
            &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
            &lt;span class="na"&gt;pods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
          &lt;span class="na"&gt;targetThresholds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
            &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
            &lt;span class="na"&gt;pods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;balance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LowNodeUtilization"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔌 the strategies you will actually use
&lt;/h2&gt;

&lt;p&gt;There are many plugins, but a handful do most of the work. Let me group them by extension point.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚖️ balance plugins
&lt;/h3&gt;

&lt;p&gt;These rebalance placement across the cluster.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔥 &lt;strong&gt;LowNodeUtilization&lt;/strong&gt;: finds underused nodes and evicts pods from overused ones, hoping they reschedule onto the quiet nodes. Note: utilization here is based on &lt;strong&gt;pod requests vs allocatable&lt;/strong&gt;, not live metrics, unless you wire up a metrics provider.&lt;/li&gt;
&lt;li&gt;🌶️ &lt;strong&gt;HighNodeUtilization&lt;/strong&gt;: the opposite idea, pack pods off underused nodes so they can be scaled down. Great with a cluster autoscaler that scales in.&lt;/li&gt;
&lt;li&gt;👯 &lt;strong&gt;RemoveDuplicates&lt;/strong&gt;: stops multiple pods of the same workload from piling onto one node, which is exactly what you want for high availability.&lt;/li&gt;
&lt;li&gt;🗺️ &lt;strong&gt;RemovePodsViolatingTopologySpreadConstraint&lt;/strong&gt;: re-balances pods so they respect your topology spread constraints again.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🧨 deschedule plugins
&lt;/h3&gt;

&lt;p&gt;These walk pods and evict the ones that no longer belong.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🚫 &lt;strong&gt;RemovePodsViolatingNodeAffinity&lt;/strong&gt;: evicts pods stuck on nodes that no longer match their node affinity.&lt;/li&gt;
&lt;li&gt;🧪 &lt;strong&gt;RemovePodsViolatingNodeTaints&lt;/strong&gt;: evicts pods that no longer tolerate a node's taints.&lt;/li&gt;
&lt;li&gt;🧲 &lt;strong&gt;RemovePodsViolatingInterPodAntiAffinity&lt;/strong&gt;: cleans up pods that now break their anti affinity rules.&lt;/li&gt;
&lt;li&gt;♻️ &lt;strong&gt;RemovePodsHavingTooManyRestarts&lt;/strong&gt;: evicts crash looping pods past a restart threshold so they can try a healthier node.&lt;/li&gt;
&lt;li&gt;⏳ &lt;strong&gt;PodLifeTime&lt;/strong&gt;: evicts pods older than a max lifetime, handy for forcing periodic recycling.&lt;/li&gt;
&lt;li&gt;🧟 &lt;strong&gt;RemoveFailedPods&lt;/strong&gt;: clears out pods stuck in a failed state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Start with &lt;code&gt;RemoveDuplicates&lt;/code&gt; and &lt;code&gt;LowNodeUtilization&lt;/code&gt;. They give the most value with the least surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛡️ the Default Evictor, your safety net
&lt;/h2&gt;

&lt;p&gt;Before any strategy evicts a pod, the &lt;strong&gt;Default Evictor&lt;/strong&gt; decides whether that pod is even allowed to be touched. This is your most important safety layer, so do not skip it.&lt;/p&gt;

&lt;p&gt;The args you will reach for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🧷 &lt;code&gt;nodeFit: true&lt;/code&gt;: only evict a pod if there is actually another node it could land on. This single setting prevents most pointless eviction loops.&lt;/li&gt;
&lt;li&gt;🔢 &lt;code&gt;minReplicas&lt;/code&gt;: never evict if fewer than this many replicas exist, so you do not knock out a lonely pod.&lt;/li&gt;
&lt;li&gt;👑 &lt;code&gt;evictSystemCriticalPods: false&lt;/code&gt;: leave system critical priority pods alone (this is the default).&lt;/li&gt;
&lt;li&gt;💾 &lt;code&gt;evictLocalStoragePods: false&lt;/code&gt;: do not evict pods using local storage, unless you really mean to.&lt;/li&gt;
&lt;li&gt;🐝 &lt;code&gt;evictDaemonSetPods: false&lt;/code&gt;: leave DaemonSet pods in place (this is the default).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;descheduler/v1alpha2"&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DeschedulerPolicy"&lt;/span&gt;
&lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
    &lt;span class="na"&gt;pluginConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DefaultEvictor"&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;nodeFit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;minReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
          &lt;span class="na"&gt;evictSystemCriticalPods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
          &lt;span class="na"&gt;evictLocalStoragePods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
          &lt;span class="na"&gt;evictDaemonSetPods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Newer releases also offer a &lt;code&gt;podProtections&lt;/code&gt; block (with &lt;code&gt;defaultDisabled&lt;/code&gt; and &lt;code&gt;extraEnabled&lt;/code&gt; lists, plus extras like &lt;code&gt;PodsWithPVC&lt;/code&gt; and &lt;code&gt;PodsWithoutPDB&lt;/code&gt;). The classic args above still work and are easier to read when you are getting started.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🚀 installing it with Helm
&lt;/h2&gt;

&lt;p&gt;The official Helm chart is the cleanest path. These commands are for the chart published by the project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm repo add descheduler https://kubernetes-sigs.github.io/descheduler/
helm repo update
helm &lt;span class="nb"&gt;install &lt;/span&gt;descheduler descheduler/descheduler &lt;span class="nt"&gt;--namespace&lt;/span&gt; kube-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now here is a complete, opinionated &lt;code&gt;values.yaml&lt;/code&gt; you can actually start from. It runs as a CronJob, enables the safe high value strategies, and keeps the Default Evictor strict.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# values.yaml&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CronJob&lt;/span&gt;
&lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*/15&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;   &lt;span class="c1"&gt;# run every 15 minutes, tune to taste&lt;/span&gt;

&lt;span class="c1"&gt;# pin the image to a known version&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.k8s.io/descheduler/descheduler&lt;/span&gt;
  &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v0.36.0&lt;/span&gt;

&lt;span class="na"&gt;deschedulerPolicyAPIVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;descheduler/v1alpha2"&lt;/span&gt;

&lt;span class="na"&gt;deschedulerPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# cluster wide eviction guard rails&lt;/span&gt;
  &lt;span class="na"&gt;maxNoOfPodsToEvictPerNode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;maxNoOfPodsToEvictPerNamespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;maxNoOfPodsToEvictTotal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;

  &lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
      &lt;span class="na"&gt;pluginConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DefaultEvictor"&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;nodeFit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="na"&gt;minReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
            &lt;span class="na"&gt;evictSystemCriticalPods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
            &lt;span class="na"&gt;evictLocalStoragePods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
            &lt;span class="na"&gt;evictDaemonSetPods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RemoveDuplicates"&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;excludeOwnerKinds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DaemonSet"&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LowNodeUtilization"&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;thresholds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
              &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
              &lt;span class="na"&gt;pods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
            &lt;span class="na"&gt;targetThresholds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
              &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
              &lt;span class="na"&gt;pods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RemovePodsHavingTooManyRestarts"&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;podRestartThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
            &lt;span class="na"&gt;includingInitContainers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;balance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RemoveDuplicates"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LowNodeUtilization"&lt;/span&gt;
        &lt;span class="na"&gt;deschedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RemovePodsHavingTooManyRestarts"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm upgrade &lt;span class="nt"&gt;--install&lt;/span&gt; descheduler descheduler/descheduler &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; kube-system &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--values&lt;/span&gt; values.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Prefer Kustomize? The project ships base manifests too. Swap the ref for the release branch that matches your version (for example &lt;code&gt;release-1.36&lt;/code&gt; for &lt;code&gt;v0.36.0&lt;/code&gt;):&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;kustomize build &lt;span class="s1"&gt;'github.com/kubernetes-sigs/descheduler/kubernetes/cronjob?ref=release-1.34'&lt;/span&gt; | kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; -
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  ⏱️ CronJob vs Deployment mode
&lt;/h2&gt;

&lt;p&gt;The descheduler can run in three shapes. Two matter day to day.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🕒 &lt;strong&gt;CronJob&lt;/strong&gt;: runs on a schedule (&lt;code&gt;schedule: "*/15 * * * *"&lt;/code&gt;) and exits. Simple, cheap, easy to reason about. Great default.&lt;/li&gt;
&lt;li&gt;♾️ &lt;strong&gt;Deployment&lt;/strong&gt;: runs continuously and re-evaluates every &lt;code&gt;deschedulingInterval&lt;/code&gt; (for example &lt;code&gt;5m&lt;/code&gt;). Pair it with leader election if you run more than one replica.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Deployment mode snippet&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;deschedulingInterval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
&lt;span class="na"&gt;leaderElection&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 If you are not sure, start with CronJob. You only move to Deployment when you want tighter, continuous reaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧪 test safely with dry run first
&lt;/h2&gt;

&lt;p&gt;Please do not point this at production blind. Run it in dry run mode and read the logs first. 🙏&lt;/p&gt;

&lt;p&gt;With Helm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm upgrade &lt;span class="nt"&gt;--install&lt;/span&gt; descheduler descheduler/descheduler &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; kube-system &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--values&lt;/span&gt; values.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; cmdOptions.dry-run&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then watch what it &lt;em&gt;would&lt;/em&gt; have done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system logs &lt;span class="nt"&gt;-l&lt;/span&gt; app.kubernetes.io/name&lt;span class="o"&gt;=&lt;/span&gt;descheduler &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will see lines naming the pods it would evict and the strategy that picked them. Tune your thresholds until the list looks sane, then turn dry run off.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧯 production safety checklist
&lt;/h2&gt;

&lt;p&gt;Before you let it evict for real, walk this list. ✅&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;PodDisruptionBudgets everywhere&lt;/strong&gt; that matters. The descheduler honors them, so a good PDB is your strongest guard against an outage.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;&lt;code&gt;nodeFit: true&lt;/code&gt;&lt;/strong&gt; so it never evicts a pod that has nowhere else to go.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;&lt;code&gt;minReplicas&lt;/code&gt; set&lt;/strong&gt; so single replica workloads are left alone.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Eviction caps&lt;/strong&gt; via &lt;code&gt;maxNoOfPodsToEvictPerNode&lt;/code&gt;, &lt;code&gt;maxNoOfPodsToEvictPerNamespace&lt;/code&gt; and &lt;code&gt;maxNoOfPodsToEvictTotal&lt;/code&gt; so a bad run cannot churn the whole cluster.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;System namespaces protected&lt;/strong&gt;, and system critical priority pods left untouched.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Sensible schedule&lt;/strong&gt;. Every few minutes is rarely needed. Every 15 to 30 minutes is plenty for most clusters.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📊 keep an eye on it
&lt;/h2&gt;

&lt;p&gt;The descheduler exposes Prometheus metrics, served on &lt;code&gt;https://localhost:10258/metrics&lt;/code&gt; by default. You can change the address with the &lt;code&gt;--binding-address&lt;/code&gt; and &lt;code&gt;--secure-port&lt;/code&gt; flags.&lt;/p&gt;

&lt;p&gt;Scrape it, then watch how many pods it evicts per run. A healthy cluster should settle into a small, steady number. A number that never drops is a signal that something keeps fighting it. ⚠️&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 the gotcha that bites everyone
&lt;/h2&gt;

&lt;p&gt;Here is the classic trap: an &lt;strong&gt;eviction and reschedule loop&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It usually looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You enable a strategy (say node affinity based) to nudge pods toward preferred nodes.&lt;/li&gt;
&lt;li&gt;Those preferred nodes are not available yet.&lt;/li&gt;
&lt;li&gt;The descheduler evicts the pod, the scheduler puts it right back where it was, and the cycle repeats forever. 🔄&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This shows up a lot with spot or autoscaled node pools, where the target nodes are still being provisioned when the eviction happens.&lt;/p&gt;

&lt;p&gt;How to avoid the loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🧷 Keep &lt;code&gt;nodeFit: true&lt;/code&gt; so it will not evict when there is no valid destination.&lt;/li&gt;
&lt;li&gt;🐢 Use a calmer schedule so the cluster has time to settle between runs.&lt;/li&gt;
&lt;li&gt;🎯 Be careful with &lt;code&gt;preferred&lt;/code&gt; affinity rules as eviction triggers, since "preferred" is never fully satisfied and can churn endlessly.&lt;/li&gt;
&lt;li&gt;🚦 Cap evictions so even a misconfiguration stays contained.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 If you see the same pods evicted over and over in dry run, fix that before going live. Dry run is exactly how you catch this.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎁 wrapping up
&lt;/h2&gt;

&lt;p&gt;The descheduler is one of those tools that quietly earns its keep. It does not replace your scheduler, it just keeps cleaning up after entropy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🧹 It evicts poorly placed pods and lets the scheduler re-place them.&lt;/li&gt;
&lt;li&gt;🛡️ It respects PDBs and the Default Evictor so it stays polite.&lt;/li&gt;
&lt;li&gt;🚀 It installs in minutes with Helm and tunes with a single values file.&lt;/li&gt;
&lt;li&gt;🧪 It is safe to trial thanks to dry run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start small. Turn on &lt;code&gt;RemoveDuplicates&lt;/code&gt; and &lt;code&gt;LowNodeUtilization&lt;/code&gt;, run in dry run, read the logs, then let it loose. Your nodes will thank you. 😄&lt;/p&gt;

&lt;p&gt;Happy clustering and stay safe! 🧹🚀&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>containers</category>
    </item>
    <item>
      <title>🧠 Demystifying Metrics in Kubernetes</title>
      <dc:creator>Hamdi (KHELIL) LION</dc:creator>
      <pubDate>Mon, 09 Feb 2026 07:48:25 +0000</pubDate>
      <link>https://dev.to/hkhelil/demystifying-metrics-in-kubernetes-id7</link>
      <guid>https://dev.to/hkhelil/demystifying-metrics-in-kubernetes-id7</guid>
      <description>&lt;p&gt;Kubernetes does not magically know when to scale your workloads 🤖&lt;br&gt;
It relies on &lt;strong&gt;metrics exposed through dedicated APIs&lt;/strong&gt; to make scaling decisions.&lt;/p&gt;

&lt;p&gt;There are &lt;strong&gt;three types of metrics&lt;/strong&gt; you need to understand:&lt;/p&gt;

&lt;p&gt;⚙️ Resource Metrics&lt;br&gt;
📊 Custom Metrics&lt;br&gt;
🌍 External Metrics&lt;/p&gt;

&lt;p&gt;Each one represents a different kind of pressure on your system.&lt;/p&gt;
&lt;h2&gt;
  
  
  🤔 Why Metrics Matter for Autoscaling
&lt;/h2&gt;

&lt;p&gt;Autoscaling in Kubernetes is mainly handled by the &lt;strong&gt;Horizontal Pod Autoscaler&lt;/strong&gt; (HPA).&lt;/p&gt;

&lt;p&gt;The HPA keeps asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Hey… are my Pods struggling?” 😅&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer comes from metrics. Without them, Kubernetes is basically guessing.&lt;/p&gt;
&lt;h2&gt;
  
  
  ⚙️ 1 Resource Metrics = Pod Health Signals
&lt;/h2&gt;

&lt;p&gt;These are the &lt;strong&gt;native Kubernetes metrics&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;They come from the &lt;strong&gt;Metrics Server&lt;/strong&gt; and only cover:&lt;/p&gt;

&lt;p&gt;🧮 CPU usage&lt;br&gt;
🧠 Memory usage&lt;/p&gt;

&lt;p&gt;They are exposed via:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;metrics.k8s.io&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  🧩 What they represent
&lt;/h3&gt;

&lt;p&gt;They describe &lt;strong&gt;resource consumption&lt;/strong&gt;, not business traffic.&lt;/p&gt;

&lt;p&gt;Your app might be slow because of a database… but CPU could still be chill 🧊&lt;/p&gt;
&lt;h3&gt;
  
  
  📄 Example HPA based on CPU
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;autoscaling/v2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HorizontalPodAutoscaler&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-hpa&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scaleTargetRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
  &lt;span class="na"&gt;minReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;maxReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Resource&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cpu&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Utilization&lt;/span&gt;
        &lt;span class="na"&gt;averageUtilization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;70&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If average CPU across Pods goes above 70 percent → more replicas 🔥&lt;/p&gt;
&lt;h3&gt;
  
  
  ✅ Pros
&lt;/h3&gt;

&lt;p&gt;Super simple&lt;br&gt;
Works out of the box&lt;/p&gt;
&lt;h3&gt;
  
  
  ❌ Limits
&lt;/h3&gt;

&lt;p&gt;CPU is not always equal to user traffic&lt;br&gt;
Memory often reacts too late&lt;/p&gt;
&lt;h2&gt;
  
  
  📊 2 Custom Metrics = Your App Talking to Kubernetes
&lt;/h2&gt;

&lt;p&gt;Custom metrics come from &lt;strong&gt;applications inside your cluster&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;They describe &lt;strong&gt;business or application load&lt;/strong&gt; 💼&lt;/p&gt;

&lt;p&gt;They are exposed through:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;custom.metrics.k8s.io&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  🔁 Typical data flow
&lt;/h3&gt;

&lt;p&gt;App exposes &lt;code&gt;/metrics&lt;/code&gt;&lt;br&gt;
Prometheus scrapes&lt;br&gt;
Prometheus Adapter maps metrics → Kubernetes API&lt;/p&gt;

&lt;p&gt;Now Kubernetes can ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How busy is this Deployment really?” 👀&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  🌐 Example 1 HTTP requests per second
&lt;/h3&gt;

&lt;p&gt;Metric in Prometheus:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;http_requests_per_second&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;🧠 What it means&lt;br&gt;
Real traffic handled by each Pod&lt;/p&gt;

&lt;p&gt;📦 In Kubernetes&lt;br&gt;
A metric attached to Pods&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pods&lt;/span&gt;
  &lt;span class="na"&gt;pods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metric&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http_requests_per_second&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AverageValue&lt;/span&gt;
      &lt;span class="na"&gt;averageValue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If each Pod handles more than 200 requests per second → scale out 🚀&lt;/p&gt;

&lt;h3&gt;
  
  
  ⏱ Example 2 Request duration
&lt;/h3&gt;

&lt;p&gt;Metric:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;request_duration_seconds&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;🧠 What it means&lt;br&gt;
Application performance and saturation&lt;/p&gt;

&lt;p&gt;Used as an Object metric:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Object&lt;/span&gt;
  &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metric&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;avg_request_duration&lt;/span&gt;
    &lt;span class="na"&gt;describedObject&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Value&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If average latency goes above 500ms → time to add Pods 🏃‍♂️&lt;/p&gt;

&lt;h3&gt;
  
  
  🧵 Example 3 Active background jobs
&lt;/h3&gt;

&lt;p&gt;Metric:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;active_background_jobs&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;🧠 What it means&lt;br&gt;
Internal workload of a worker&lt;/p&gt;

&lt;p&gt;Each Pod reports its own load, and HPA scales when workers are overloaded 📈&lt;/p&gt;
&lt;h3&gt;
  
  
  ✅ Pros
&lt;/h3&gt;

&lt;p&gt;Scaling reflects real app behavior&lt;br&gt;
Way smarter than CPU only&lt;/p&gt;
&lt;h3&gt;
  
  
  ❌ Limits
&lt;/h3&gt;

&lt;p&gt;Requires Prometheus + Prometheus Adapter&lt;br&gt;
More components to maintain 😬&lt;/p&gt;
&lt;h2&gt;
  
  
  🌍 3 External Metrics = Work Waiting Outside the Cluster
&lt;/h2&gt;

&lt;p&gt;External metrics come from &lt;strong&gt;systems outside Kubernetes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;They are exposed via:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;external.metrics.k8s.io&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;These metrics describe work your Pods must process, even if it lives elsewhere 🌎&lt;/p&gt;
&lt;h3&gt;
  
  
  📬 Example 1 SQS queue length
&lt;/h3&gt;

&lt;p&gt;Metric:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ApproximateNumberOfMessagesVisible&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;🧠 What it means&lt;br&gt;
Number of messages waiting in the queue&lt;/p&gt;

&lt;p&gt;In Kubernetes&lt;br&gt;
A global metric not tied to specific Pods&lt;/p&gt;

&lt;p&gt;If the queue grows → spawn more workers 💪&lt;/p&gt;
&lt;h3&gt;
  
  
  🐘 Example 2 Kafka consumer lag
&lt;/h3&gt;

&lt;p&gt;Metric:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kafka_consumer_lag&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;🧠 What it means&lt;br&gt;
Delay between producers and consumers&lt;/p&gt;

&lt;p&gt;More lag = your consumers are falling behind 😱&lt;br&gt;
Scale them up!&lt;/p&gt;
&lt;h3&gt;
  
  
  📦 Example 3 Redis job queue size
&lt;/h3&gt;

&lt;p&gt;Metric:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;redis_list_length&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;🧠 What it means&lt;br&gt;
Number of jobs waiting in Redis queues&lt;/p&gt;

&lt;p&gt;Perfect for worker autoscaling 🔄&lt;/p&gt;
&lt;h3&gt;
  
  
  ⏰ Example 4 Time based scaling
&lt;/h3&gt;

&lt;p&gt;Scale more Pods during office hours&lt;br&gt;
Scale down at night 🌙&lt;/p&gt;

&lt;p&gt;This is also treated as an external signal, because it’s not tied to Pod resource usage.&lt;/p&gt;
&lt;h2&gt;
  
  
  🧮 How HPA Uses These Metrics
&lt;/h2&gt;

&lt;p&gt;HPA periodically queries:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric Type&lt;/th&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Resource&lt;/td&gt;
&lt;td&gt;metrics.k8s.io&lt;/td&gt;
&lt;td&gt;CPU and memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;custom.metrics.k8s.io&lt;/td&gt;
&lt;td&gt;App level load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External&lt;/td&gt;
&lt;td&gt;external.metrics.k8s.io&lt;/td&gt;
&lt;td&gt;Event or system load&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then it calculates how many replicas you need 📐&lt;/p&gt;
&lt;h2&gt;
  
  
  ⚡ KEDA vs Prometheus Adapter
&lt;/h2&gt;

&lt;p&gt;Here comes the game changer 🎮&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KEDA&lt;/strong&gt; (Kubernetes Event Driven Autoscaling) focuses on &lt;strong&gt;event driven autoscaling&lt;/strong&gt; and makes custom and external metrics way easier to use.&lt;/p&gt;
&lt;h3&gt;
  
  
  🧩 With Prometheus Adapter
&lt;/h3&gt;

&lt;p&gt;You must:&lt;/p&gt;

&lt;p&gt;Run Prometheus&lt;br&gt;
Install and configure the Adapter&lt;br&gt;
Write mapping rules&lt;br&gt;
Manage RBAC and certs&lt;/p&gt;

&lt;p&gt;It works, but it’s heavy 🏋️&lt;/p&gt;
&lt;h3&gt;
  
  
  ⚡ With KEDA
&lt;/h3&gt;

&lt;p&gt;You define a &lt;strong&gt;ScaledObject&lt;/strong&gt; and KEDA does the magic ✨&lt;/p&gt;

&lt;p&gt;Example SQS scaler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keda.sh/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ScaledObject&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worker-scaler&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scaleTargetRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worker&lt;/span&gt;
  &lt;span class="na"&gt;minReplicaCount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="na"&gt;maxReplicaCount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
  &lt;span class="na"&gt;triggers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-sqs-queue&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;queueURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://sqs.eu-west-1.amazonaws.com/123/my-queue&lt;/span&gt;
      &lt;span class="na"&gt;queueLength&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10"&lt;/span&gt;
      &lt;span class="na"&gt;awsRegion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eu-west-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;KEDA fetches the metric&lt;br&gt;
Exposes it to Kubernetes&lt;br&gt;
Creates and manages the HPA&lt;br&gt;
Handles provider auth 🔐&lt;/p&gt;

&lt;p&gt;All without Prometheus Adapter 🤯&lt;/p&gt;

&lt;h2&gt;
  
  
  🧭 When to Use What
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If you want to scale on…&lt;/th&gt;
&lt;th&gt;Use…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU or memory&lt;/td&gt;
&lt;td&gt;Resource metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP traffic or app load&lt;/td&gt;
&lt;td&gt;Custom metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queues, streams, SaaS&lt;/td&gt;
&lt;td&gt;External metrics + KEDA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Events or scale to zero&lt;/td&gt;
&lt;td&gt;KEDA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  🎯 Final Takeaway
&lt;/h2&gt;

&lt;p&gt;Kubernetes becomes truly powerful when scaling is driven by &lt;strong&gt;real workload signals&lt;/strong&gt;, not just CPU.&lt;/p&gt;

&lt;p&gt;⚙️ Resource metrics are the starting point&lt;br&gt;
📊 Custom metrics bring application awareness&lt;br&gt;
🌍 External metrics unlock event driven architectures&lt;br&gt;
⚡ KEDA makes advanced autoscaling simple and production friendly&lt;/p&gt;

&lt;p&gt;Once you understand these three metric types, autoscaling stops being magic and becomes a design tool you control 💡🚀&lt;/p&gt;

&lt;p&gt;Happy clustering :)&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cloud</category>
      <category>keda</category>
      <category>devops</category>
    </item>
    <item>
      <title>Building a Kubernetes homelab the hard but right way 🧱☸️</title>
      <dc:creator>Hamdi (KHELIL) LION</dc:creator>
      <pubDate>Tue, 20 Jan 2026 10:32:46 +0000</pubDate>
      <link>https://dev.to/hkhelil/building-a-kubernetes-homelab-the-hard-but-right-way-308k</link>
      <guid>https://dev.to/hkhelil/building-a-kubernetes-homelab-the-hard-but-right-way-308k</guid>
      <description>&lt;p&gt;If you have been following my previous articles, you already know the drill 😄&lt;br&gt;
I like setups that are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;boring&lt;/li&gt;
&lt;li&gt;repeatable&lt;/li&gt;
&lt;li&gt;close to real production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article is not about spinning up a quick cluster.&lt;br&gt;
It is about &lt;strong&gt;building a platform&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A platform you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rebuild from scratch&lt;/li&gt;
&lt;li&gt;scale without fear&lt;/li&gt;
&lt;li&gt;explain to a client or a teammate&lt;/li&gt;
&lt;li&gt;reuse in real consulting missions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And yes, it runs at home 👀&lt;/p&gt;

&lt;h2&gt;
  
  
  The mindset first 🧠
&lt;/h2&gt;

&lt;p&gt;Before touching any tool, I decided on one rule:&lt;/p&gt;

&lt;p&gt;👉 no manual action that I cannot reproduce with code&lt;/p&gt;

&lt;p&gt;That single rule drives everything else.&lt;/p&gt;

&lt;p&gt;So the stack becomes very natural:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Proxmox for virtualization&lt;/li&gt;
&lt;li&gt;Terraform to create machines&lt;/li&gt;
&lt;li&gt;Cloud Init to bootstrap the OS&lt;/li&gt;
&lt;li&gt;Kubespray to install Kubernetes&lt;/li&gt;
&lt;li&gt;Helmfile for everything after day 1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each layer has &lt;strong&gt;one job only&lt;/strong&gt;.&lt;br&gt;
No overlap. No shortcuts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The big picture 🗺️
&lt;/h2&gt;

&lt;p&gt;Here is what actually happens when I type terraform apply 👇&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Proxmox clones VMs from a cloud init template&lt;/li&gt;
&lt;li&gt;Each VM boots with the right user, SSH keys and network&lt;/li&gt;
&lt;li&gt;Nodes are reachable immediately&lt;/li&gt;
&lt;li&gt;Kubespray turns them into a real HA Kubernetes cluster&lt;/li&gt;
&lt;li&gt;Helmfile deploys platform components on top&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you understand that flow, debugging becomes easy and scaling becomes boring.&lt;br&gt;
And boring is good 😌&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 Proxmox as a real compute layer ⚙️
&lt;/h2&gt;

&lt;p&gt;I treat Proxmox like a private cloud, not like a lab UI.&lt;/p&gt;

&lt;p&gt;No clicking. No guessing.&lt;/p&gt;

&lt;p&gt;Terraform talks directly to the Proxmox API and creates Kubernetes nodes exactly the same way every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I am doing here
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;cloning from a golden cloud init template&lt;/li&gt;
&lt;li&gt;injecting user data via snippets&lt;/li&gt;
&lt;li&gt;assigning static IPs&lt;/li&gt;
&lt;li&gt;tagging nodes for clarity&lt;/li&gt;
&lt;li&gt;keeping compute concerns separate from Kubernetes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this stage, Kubernetes does not exist yet.&lt;br&gt;
And that is exactly what I want 👍&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 The cluster is just data 📐
&lt;/h2&gt;

&lt;p&gt;This is one of my favorite parts.&lt;/p&gt;

&lt;p&gt;The cluster topology lives in a tfvars file.&lt;br&gt;
No logic. No magic. Just data.&lt;/p&gt;

&lt;p&gt;This is extremely important because it means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the same code can create one cluster or ten&lt;/li&gt;
&lt;li&gt;topology changes do not require refactoring&lt;/li&gt;
&lt;li&gt;environments stay consistent over time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding a node or a whole new cluster is just editing data 🔁&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 Reusable Terraform modules 🧩
&lt;/h2&gt;

&lt;p&gt;Everything goes through a compute module.&lt;/p&gt;

&lt;p&gt;Terraform is responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;machines&lt;/li&gt;
&lt;li&gt;networking&lt;/li&gt;
&lt;li&gt;bootstrapping access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And nothing else.&lt;/p&gt;

&lt;p&gt;Once the VMs exist, Terraform is basically done.&lt;/p&gt;

&lt;p&gt;This clear boundary avoids a lot of confusion later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 Kubespray does the heavy lifting ☸️
&lt;/h2&gt;

&lt;p&gt;Kubespray is where Kubernetes actually comes to life.&lt;/p&gt;

&lt;p&gt;It handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HA control plane&lt;/li&gt;
&lt;li&gt;stacked etcd&lt;/li&gt;
&lt;li&gt;container runtime&lt;/li&gt;
&lt;li&gt;CNI and kubelet configuration&lt;/li&gt;
&lt;li&gt;sane defaults and hardening&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not a quick kubeadm script.&lt;br&gt;
This is a production grade installer that I fully trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 Bootstrap only the essentials 🚦
&lt;/h2&gt;

&lt;p&gt;At cluster creation time, I only enable what is strictly required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;metrics server&lt;/li&gt;
&lt;li&gt;ingress nginx&lt;/li&gt;
&lt;li&gt;metallb&lt;/li&gt;
&lt;li&gt;gateway api&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing fancy. Nothing opinionated.&lt;/p&gt;

&lt;p&gt;The goal here is to have a &lt;strong&gt;usable Kubernetes cluster&lt;/strong&gt;, not a fully loaded platform.&lt;/p&gt;

&lt;p&gt;Everything else can wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 Day 2 is simple and explicit with Helmfile 📦
&lt;/h2&gt;

&lt;p&gt;This is important.&lt;/p&gt;

&lt;p&gt;There is &lt;strong&gt;no fancy GitOps setup here&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No Argo CD bootstrap.&lt;br&gt;
No Flux managing Flux.&lt;br&gt;
No recursive GitOps inception 😄&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Helmfile is run explicitly&lt;/li&gt;
&lt;li&gt;changes are intentional&lt;/li&gt;
&lt;li&gt;failures are visible&lt;/li&gt;
&lt;li&gt;debugging stays simple&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a single cluster or a homelab, this is often the best tradeoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  When GitOps really makes sense 🚀
&lt;/h2&gt;

&lt;p&gt;Now, things change when you have &lt;strong&gt;multiple clusters to manage&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At that point, this setup becomes extremely powerful.&lt;/p&gt;

&lt;p&gt;Because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform already creates the machines&lt;/li&gt;
&lt;li&gt;Kubespray already installs Kubernetes&lt;/li&gt;
&lt;li&gt;Helmfile already describes the platform state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can easily plug a GitOps layer on top to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create a new cluster from scratch&lt;/li&gt;
&lt;li&gt;apply a standard baseline&lt;/li&gt;
&lt;li&gt;end up with a ready to use Kubernetes platform&lt;/li&gt;
&lt;li&gt;with almost zero manual action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it as:&lt;br&gt;
👉 one command to pop a full Kubernetes cluster, ready for workloads&lt;/p&gt;

&lt;p&gt;This is where Argo CD or Flux start to shine, but &lt;strong&gt;only when the scale justifies it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Until then, keeping things simple is often the most mature choice 🧘&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I like this approach so much ❤️
&lt;/h2&gt;

&lt;p&gt;Because it grows with you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;simple when you have one cluster&lt;/li&gt;
&lt;li&gt;scalable when you have many&lt;/li&gt;
&lt;li&gt;understandable at every step&lt;/li&gt;
&lt;li&gt;close to how real platforms are built&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No magic.&lt;br&gt;
No hidden automation.&lt;br&gt;
Just solid engineering.&lt;/p&gt;

&lt;p&gt;If you can run this at home, you can run it anywhere 🌍&lt;/p&gt;

&lt;h2&gt;
  
  
  What is next 🔮
&lt;/h2&gt;

&lt;p&gt;In upcoming articles I will dive into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;storage choices and csi&lt;/li&gt;
&lt;li&gt;dns and external-dns&lt;/li&gt;
&lt;li&gt;security with kyverno&lt;/li&gt;
&lt;li&gt;observability and metrics&lt;/li&gt;
&lt;li&gt;multi cluster patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay tuned 👋 and happy clustering :) &lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cloud</category>
      <category>devops</category>
      <category>containers</category>
    </item>
    <item>
      <title>Building a Kubernetes HomeLab - The Hard Way - DNS first, or everything else lies to you 🔥🌐</title>
      <dc:creator>Hamdi (KHELIL) LION</dc:creator>
      <pubDate>Mon, 22 Dec 2025 08:08:54 +0000</pubDate>
      <link>https://dev.to/hkhelil/building-a-kubernetes-homelab-the-hard-way-dns-first-or-everything-else-lies-to-you-3ng1</link>
      <guid>https://dev.to/hkhelil/building-a-kubernetes-homelab-the-hard-way-dns-first-or-everything-else-lies-to-you-3ng1</guid>
      <description>&lt;p&gt;Let’s be very clear from the start 👇&lt;br&gt;
If DNS is shaky, everything above it becomes unreliable, misleading, and borderline hostile.&lt;/p&gt;

&lt;p&gt;You will blame Kubernetes.&lt;br&gt;
You will blame cert-manager.&lt;br&gt;
You will blame yourself.&lt;/p&gt;

&lt;p&gt;And it will still be DNS 😅&lt;/p&gt;

&lt;p&gt;This article builds a real DNS layer, outside the cluster, the way it is done in production environments.&lt;br&gt;
Not fancy. Not clever. Just solid.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;DNS is not a feature, it is the foundation 🧱&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most homelabs treat DNS like a checkbox:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;router default&lt;/li&gt;
&lt;li&gt;maybe 1.1.1.1&lt;/li&gt;
&lt;li&gt;maybe 8.8.8.8&lt;/li&gt;
&lt;li&gt;call it a day&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This works until you introduce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple Proxmox nodes&lt;/li&gt;
&lt;li&gt;Kubernetes bootstrap&lt;/li&gt;
&lt;li&gt;internal TLS&lt;/li&gt;
&lt;li&gt;ExternalDNS&lt;/li&gt;
&lt;li&gt;service to service communication
At that point, DNS stops being optional.
It becomes infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this layer lies, everything else lies with it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;what I assume before we start breaking things 🔧&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This article assumes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you are comfortable with Linux and SSH&lt;/li&gt;
&lt;li&gt;you control your LAN&lt;/li&gt;
&lt;li&gt;you can assign static IPs&lt;/li&gt;
&lt;li&gt;you are fine rebuilding things if needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Network baseline used in examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LAN CIDR: 192.168.1.0/24&lt;/li&gt;
&lt;li&gt;router: 192.168.1.1&lt;/li&gt;
&lt;li&gt;DNS node: 192.168.1.169&lt;/li&gt;
&lt;li&gt;internal domain: home.arpa&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why home.arpa?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;defined by RFC 8375&lt;/li&gt;
&lt;li&gt;designed for private networks&lt;/li&gt;
&lt;li&gt;no collision with public DNS&lt;/li&gt;
&lt;li&gt;predictable behavior across operating systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;.local is convenient.&lt;br&gt;
.local is also a trap ☠️&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;how DNS flows in this homelab, no magic involved 🧭&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The DNS request path is intentionally boring:&lt;/p&gt;

&lt;p&gt;client&lt;br&gt;
→ Pi-hole&lt;br&gt;
→ Unbound&lt;br&gt;
→ root servers&lt;br&gt;
→ TLD servers&lt;br&gt;
→ authoritative servers&lt;/p&gt;

&lt;p&gt;Why this design?&lt;/p&gt;

&lt;p&gt;Pi-hole sits in front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;single DNS endpoint for the entire LAN&lt;/li&gt;
&lt;li&gt;full visibility into queries&lt;/li&gt;
&lt;li&gt;local DNS records for infrastructure&lt;/li&gt;
&lt;li&gt;API compatible with ExternalDNS later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unbound sits behind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;full recursive resolver&lt;/li&gt;
&lt;li&gt;DNSSEC validation&lt;/li&gt;
&lt;li&gt;zero dependency on Google or Cloudflare&lt;/li&gt;
&lt;li&gt;behaves like enterprise DNS stacks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One DNS server for the LAN:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deterministic behavior&lt;/li&gt;
&lt;li&gt;easier troubleshooting&lt;/li&gt;
&lt;li&gt;required for Kubernetes stability&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;building the DNS node, properly and calmly 🧱
A Raspberry Pi that does exactly one thing 🥧&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Reserve a static IP from your router:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hostname: dns-01&lt;/li&gt;
&lt;li&gt;ip: 192.168.1.169&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Update the system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt update
sudo apt upgrade -y
sudo apt install curl gnupg lsb-release -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This node is sacred:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DNS only&lt;/li&gt;
&lt;li&gt;no Docker&lt;/li&gt;
&lt;li&gt;no Kubernetes&lt;/li&gt;
&lt;li&gt;no experiments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If DNS breaks, you want zero doubt about the cause.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;installing Pi-hole, the DNS entry point 🚦&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pi-hole will be the only DNS server exposed to the LAN.&lt;/p&gt;

&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -sSL https://install.pi-hole.net | bash

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During the installer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;interface: eth0&lt;/li&gt;
&lt;li&gt;upstream DNS: custom&lt;/li&gt;
&lt;li&gt;do not select any public DNS provider&lt;/li&gt;
&lt;li&gt;enable the web admin interface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://192.168.1.169/admin

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Immediately change the admin password:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pihole -a -p
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this stage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pi-hole is answering DNS queries&lt;/li&gt;
&lt;li&gt;but it should not resolve anything yet&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;installing Unbound, the actual DNS resolver 🔁&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Unbound will do the real work: recursive resolution and DNSSEC validation.&lt;/p&gt;

&lt;p&gt;Install Unbound:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt install unbound -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a dedicated configuration for Pi-hole:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo tee /etc/unbound/unbound.conf.d/pi-hole.conf &amp;gt; /dev/null &amp;lt;&amp;lt;EOF
server:
  interface: 127.0.0.1
  port: 5335
  do-ip4: yes
  do-ip6: no
  do-udp: yes
  do-tcp: yes
  root-hints: "/var/lib/unbound/root.hints"
  harden-glue: yes
  harden-dnssec-stripped: yes
  use-caps-for-id: yes
  edns-buffer-size: 1232
  prefetch: yes
  verbosity: 1

forward-zone:
  name: "."
  forward-addr: 0.0.0.0
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fetch root hints:&lt;/p&gt;

&lt;p&gt;sudo curl -o /var/lib/unbound/root.hints &lt;a href="https://www.internic.net/domain/named.root" rel="noopener noreferrer"&gt;https://www.internic.net/domain/named.root&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enable and start Unbound:&lt;/p&gt;

&lt;p&gt;sudo systemctl restart unbound&lt;br&gt;
sudo systemctl enable unbound&lt;/p&gt;

&lt;p&gt;At this point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unbound listens on 127.0.0.1:5335&lt;/li&gt;
&lt;li&gt;performs full recursive resolution&lt;/li&gt;
&lt;li&gt;validates DNSSEC&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;connecting Pi-hole to Unbound, the clean way 🔗&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the Pi-hole admin UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;settings&lt;/li&gt;
&lt;li&gt;dns&lt;/li&gt;
&lt;li&gt;upstream DNS servers&lt;/li&gt;
&lt;li&gt;custom server: 127.0.0.1#5335&lt;/li&gt;
&lt;li&gt;disable all other upstream resolvers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the chain is complete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clients talk to Pi-hole&lt;/li&gt;
&lt;li&gt;Pi-hole forwards to Unbound&lt;/li&gt;
&lt;li&gt;Unbound talks to the internet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No leaks.&lt;br&gt;
No shortcuts.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;naming things like an adult 🏷️&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;internal DNS records&lt;/p&gt;

&lt;p&gt;Define local DNS records in Pi-hole:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;proxmox-01.home.arpa   192.168.1.41
proxmox-02.home.arpa   192.168.1.42
proxmox-03.home.arpa   192.168.1.43
nas-01.home.arpa       192.168.1.50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ip plan&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;component   | ip
router          | 192.168.1.1
dns-01          | 192.168.1.169
proxmox-01  | 192.168.1.41
proxmox-02  | 192.168.1.42
proxmox-03  | 192.168.1.43
synology    | 192.168.1.50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Predictable naming beats clever naming.&lt;br&gt;
Every single time.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;proof before trust, always 🔍&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Test recursive resolution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dig google.com @192.168.1.169

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test DNSSEC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dig dnssec-failed.org @192.168.1.169

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SERVFAIL means DNSSEC is working ✅
Test internal names:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dig proxmox-01.home.arpa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch live DNS traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pihole -t
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If something breaks later, this is where you start.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;how people accidentally sabotage their own DNS ☠️&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Adding a public DNS as secondary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;internal names stop resolving&lt;/li&gt;
&lt;li&gt;ExternalDNS becomes unreliable&lt;/li&gt;
&lt;li&gt;cert-manager fails silently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using .local:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mDNS conflicts&lt;/li&gt;
&lt;li&gt;inconsistent resolution&lt;/li&gt;
&lt;li&gt;debugging hell&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running DNS inside Kubernetes first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bootstrap deadlock&lt;/li&gt;
&lt;li&gt;cluster depends on itself&lt;/li&gt;
&lt;li&gt;guaranteed pain&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;why this DNS layer unlocks everything else 🚀&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With this setup in place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Proxmox nodes resolve consistently&lt;/li&gt;
&lt;li&gt;Kubernetes nodes bootstrap cleanly&lt;/li&gt;
&lt;li&gt;ExternalDNS can manage records safely&lt;/li&gt;
&lt;li&gt;cert-manager can issue internal certificates&lt;/li&gt;
&lt;li&gt;ingress becomes predictable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the point where the homelab stops feeling fragile.&lt;/p&gt;

&lt;p&gt;Next article:&lt;br&gt;
Teaching your router who’s boss 🧠📡&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;lessons learned the hard way, so you do not have to 😅&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;DNS should be boring.&lt;br&gt;
Silent.&lt;br&gt;
Predictable.&lt;/p&gt;

&lt;p&gt;If DNS is exciting, something is wrong.&lt;/p&gt;

&lt;p&gt;This setup is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reproducible&lt;/li&gt;
&lt;li&gt;observable&lt;/li&gt;
&lt;li&gt;production-inspired&lt;/li&gt;
&lt;li&gt;intentionally boring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hard now.&lt;br&gt;
Peace later.&lt;/p&gt;

&lt;p&gt;Happy Clustering :) &lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>homelab</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>🏗️ Setting Up a Proxmox Cluster for My Homelab Kubernetes Environment</title>
      <dc:creator>Hamdi (KHELIL) LION</dc:creator>
      <pubDate>Wed, 05 Nov 2025 06:27:25 +0000</pubDate>
      <link>https://dev.to/hkhelil/setting-up-a-proxmox-cluster-for-my-homelab-kubernetes-environment-1jop</link>
      <guid>https://dev.to/hkhelil/setting-up-a-proxmox-cluster-for-my-homelab-kubernetes-environment-1jop</guid>
      <description>&lt;p&gt;Hey everyone 👋&lt;/p&gt;

&lt;p&gt;Now that you know the hardware powering my homelab&lt;br&gt;
it is time to show how I turned those machines into a Proxmox cluster ready to host a production style Kubernetes environment 🧑‍💻⚙️&lt;/p&gt;

&lt;p&gt;This article covers the entire setup&lt;br&gt;
including fixes for a few annoying real world problems 😅&lt;/p&gt;

&lt;p&gt;Let’s get into it 🚀&lt;/p&gt;

&lt;p&gt;✅ Step 1. Update BIOS on Lenovo Tiny Nodes&lt;/p&gt;

&lt;p&gt;Before installing Proxmox&lt;br&gt;
I strongly recommend updating the BIOS of each node&lt;/p&gt;

&lt;p&gt;Why it matters&lt;br&gt;
✅ fixes stability bugs&lt;br&gt;
✅ improves CPU performance and thermal handling&lt;br&gt;
✅ avoids random shutdowns or throttling&lt;br&gt;
✅ unlocks better hardware support&lt;/p&gt;

&lt;p&gt;Old BIOS = unpredictable issues later&lt;br&gt;
Flash now and enjoy peace later ✅&lt;/p&gt;

&lt;p&gt;✅ Step 2. Install Proxmox VE&lt;/p&gt;

&lt;p&gt;I installed Proxmox VE on each of my three Lenovo M920q nodes&lt;br&gt;
using this simple naming and addressing plan:&lt;/p&gt;

&lt;p&gt;Node    Hostname    IP&lt;/p&gt;

&lt;p&gt;Node 1  pve1    192.168.1.21&lt;br&gt;
Node 2  pve2    192.168.1.22&lt;br&gt;
Node 3  pve3    192.168.1.23&lt;/p&gt;

&lt;p&gt;DNS points to my Raspberry Pi ✅&lt;/p&gt;

&lt;p&gt;Installation takes a few minutes&lt;br&gt;
and Proxmox is ready through a browser 🎉&lt;/p&gt;

&lt;p&gt;✅ Step 3. Fix NIC Offloading Stability Issues&lt;/p&gt;

&lt;p&gt;This one hit me hard 😬&lt;br&gt;
I kept encountering:&lt;/p&gt;

&lt;p&gt;❌ random node connectivity loss&lt;br&gt;
❌ nodes freezing during network traffic&lt;br&gt;
❌ SSH dropping without reason&lt;/p&gt;

&lt;p&gt;Cause&lt;br&gt;
👉 NIC offloading on Intel network adapters&lt;/p&gt;

&lt;p&gt;Fix using this excellent community script&lt;br&gt;
&lt;a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=nic-offloading-fix" rel="noopener noreferrer"&gt;https://community-scripts.github.io/ProxmoxVE/scripts?id=nic-offloading-fix&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After applying this&lt;br&gt;
the cluster became rock solid ✅&lt;/p&gt;

&lt;p&gt;If you run Lenovo Tiny&lt;br&gt;
this fix is almost mandatory 👀&lt;/p&gt;

&lt;p&gt;✅ Step 4. Create the Proxmox Cluster&lt;/p&gt;

&lt;p&gt;On node pve1:&lt;/p&gt;

&lt;p&gt;pvecm create homelab-cluster&lt;/p&gt;

&lt;p&gt;On other nodes:&lt;/p&gt;

&lt;p&gt;pvecm add 192.168.1.21&lt;/p&gt;

&lt;p&gt;Now&lt;br&gt;
I can manage all nodes from a single interface ✅&lt;br&gt;
VMs can migrate easily between hosts ✅&lt;br&gt;
Monitoring is unified ✅&lt;/p&gt;

&lt;p&gt;Production vibes at home 😎&lt;/p&gt;

&lt;p&gt;✅ Step 5. Flat Network Setup First&lt;/p&gt;

&lt;p&gt;Managed Switch Comes Later&lt;/p&gt;

&lt;p&gt;For now&lt;br&gt;
the network is simple:&lt;/p&gt;

&lt;p&gt;one Gigabit flat network&lt;/p&gt;

&lt;p&gt;vmbr0 bridging the physical NIC&lt;/p&gt;

&lt;p&gt;DHCP/static addressing for VMs&lt;/p&gt;

&lt;p&gt;Benefits&lt;br&gt;
✅ simple&lt;br&gt;
✅ stable&lt;br&gt;
✅ easy to troubleshoot&lt;/p&gt;

&lt;p&gt;Future upgrade:&lt;br&gt;
🔹 Managed PoE switch&lt;br&gt;
🔹 VLAN segmentation for Kubernetes traffic&lt;br&gt;
🔹 Insights and better monitoring&lt;/p&gt;

&lt;p&gt;Great now&lt;br&gt;
but room to grow ✅&lt;/p&gt;

&lt;p&gt;✅ Step 6. Main Storage: Synology NAS (NFS + iSCSI)&lt;/p&gt;

&lt;p&gt;Persistent workloads require persistent storage&lt;br&gt;
so my Synology NAS handles all Kubernetes storage&lt;/p&gt;

&lt;p&gt;✅ NFS for most apps&lt;br&gt;
✅ iSCSI for database style block volumes&lt;br&gt;
✅ RAID and snapshots for safety&lt;br&gt;
✅ Backup target for Proxmox VMs&lt;/p&gt;

&lt;p&gt;The NAS is the data backbone of the homelab&lt;br&gt;
exactly how storage is handled in production 🚀&lt;/p&gt;

&lt;p&gt;✅ Step 7. Automated Ubuntu 25.x Templates With Packer&lt;/p&gt;

&lt;p&gt;To avoid manual VM setup&lt;br&gt;
I automated template building using Packer&lt;/p&gt;

&lt;p&gt;Operating system&lt;br&gt;
🧩 Ubuntu Server 25.x&lt;br&gt;
Modern kernel and great container support&lt;/p&gt;

&lt;p&gt;My Packer template creates VMs that are:&lt;br&gt;
✔ cloud init ready&lt;br&gt;
✔ qemu guest agent installed&lt;br&gt;
✔ ssh enabled&lt;br&gt;
✔ updated and hardened&lt;br&gt;
✔ swap disabled for Kubernetes&lt;/p&gt;

&lt;p&gt;This allows me to deploy&lt;br&gt;
a brand new Kubernetes node VM in under 30 seconds 🤯&lt;/p&gt;

&lt;p&gt;Fast&lt;br&gt;
repeatable&lt;br&gt;
version controlled&lt;/p&gt;

&lt;p&gt;Infrastructure as code for real 😁&lt;/p&gt;

&lt;p&gt;⚠️ Important Real World Gotchas&lt;/p&gt;

&lt;p&gt;Here are issues I personally hit&lt;br&gt;
and how I fixed them ✅&lt;/p&gt;

&lt;p&gt;Problem Cause   Solution&lt;/p&gt;

&lt;p&gt;Node freezes or network drops   NIC offloading  Apply NIC fix ✅&lt;br&gt;
Unexpected reboots or throttling    Old BIOS    Flash BIOS ✅&lt;br&gt;
High CPU temps under load   Small chassis   Tune cooling profile ✅&lt;br&gt;
VMs or pods OOM Overcommit  Reserve RAM for hypervisor ✅&lt;/p&gt;

&lt;p&gt;Homelabs teach you technology&lt;br&gt;
by breaking and fixing it&lt;br&gt;
the best learning path 🔥&lt;/p&gt;

&lt;p&gt;✅ What We Have Achieved So Far&lt;/p&gt;

&lt;p&gt;Feature Status&lt;/p&gt;

&lt;p&gt;Three node Proxmox cluster  ✅&lt;br&gt;
Network stability   ✅&lt;br&gt;
NAS integrated for storage  ✅&lt;br&gt;
Custom VM templates ✅&lt;br&gt;
Automation friendly ✅&lt;br&gt;
Kubernetes ready foundation ✅&lt;/p&gt;

&lt;p&gt;A real mini datacenter is now running in my home&lt;br&gt;
quietly&lt;br&gt;
efficiently&lt;br&gt;
and fully under my control 🏡✨&lt;/p&gt;

&lt;p&gt;✅ Coming Next&lt;/p&gt;

&lt;p&gt;Deploying Kubernetes with Kubespray&lt;/p&gt;

&lt;p&gt;In the next article you will see:&lt;/p&gt;

&lt;p&gt;🔥 3 high availability control plane nodes&lt;br&gt;
🔥 6 workers ready for workloads&lt;br&gt;
🔥 DNS fully internal&lt;br&gt;
🔥 main storage already integrated&lt;br&gt;
🔥 GitOps and monitoring soon after&lt;/p&gt;

&lt;p&gt;This is when the real fun begins 😎🔥&lt;/p&gt;

&lt;p&gt;Stay tuned&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cloud</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title>🧰 The Hardware That Powers My Homelab Kubernetes Cluster</title>
      <dc:creator>Hamdi (KHELIL) LION</dc:creator>
      <pubDate>Tue, 28 Oct 2025 09:44:55 +0000</pubDate>
      <link>https://dev.to/hkhelil/the-hardware-that-powers-my-homelab-kubernetes-cluster-45pb</link>
      <guid>https://dev.to/hkhelil/the-hardware-that-powers-my-homelab-kubernetes-cluster-45pb</guid>
      <description>&lt;p&gt;Hey everyone 👋&lt;/p&gt;

&lt;p&gt;Before diving deeper into the software stack of my homelab&lt;br&gt;
I want to fully introduce the physical components that make this whole project come alive 🧑‍🔧&lt;/p&gt;

&lt;p&gt;This article focuses entirely on &lt;strong&gt;hardware choices&lt;/strong&gt; and &lt;strong&gt;why each element matters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let’s explore the building blocks of my mini data center 🏡⚡&lt;/p&gt;


&lt;h2&gt;
  
  
  🖥️ The Compute Layer
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Three Lenovo ThinkCentre M920q Tiny
&lt;/h3&gt;

&lt;p&gt;These three machines are the heart of the homelab&lt;br&gt;
They provide the CPU and memory resources needed to run Kubernetes workloads inside virtual machines&lt;/p&gt;

&lt;p&gt;Why I chose them&lt;br&gt;
✅ compact and silent&lt;br&gt;
✅ excellent performance per watt&lt;br&gt;
✅ reliable and low power for 24x7&lt;br&gt;
✅ scalable memory support&lt;/p&gt;

&lt;p&gt;Specs per node&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Intel CPU with multiple cores&lt;/li&gt;
&lt;li&gt;32 GB RAM&lt;/li&gt;
&lt;li&gt;NVMe SSD for fast VM storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together they give me:&lt;br&gt;
🧠 96 GB RAM total&lt;br&gt;
⚡ 36 vCPUs total&lt;/p&gt;

&lt;p&gt;This allows me to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;simulate real production workloads&lt;/li&gt;
&lt;li&gt;run 3 HA control plane nodes&lt;/li&gt;
&lt;li&gt;run 6 worker nodes&lt;/li&gt;
&lt;li&gt;add utility VMs when needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Small machines&lt;br&gt;
big power 😁&lt;/p&gt;


&lt;h2&gt;
  
  
  🌐 The Networking Layer
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Gigabit Switch Today
&lt;/h3&gt;

&lt;p&gt;Managed PoE Switch Tomorrow&lt;/p&gt;

&lt;p&gt;Right now my network is built on a simple &lt;strong&gt;Gigabit Ethernet switch&lt;/strong&gt;&lt;br&gt;
Flat network&lt;br&gt;
no VLANs&lt;br&gt;
no advanced routing&lt;/p&gt;

&lt;p&gt;This keeps things:&lt;br&gt;
✅ simple to maintain&lt;br&gt;
✅ easy to debug&lt;br&gt;
✅ perfectly functional for a first iteration&lt;/p&gt;

&lt;p&gt;But I am already thinking ahead 👀&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;managed switch with PoE&lt;/strong&gt; is on my radar because it would allow:&lt;br&gt;
🧩 VLAN segmentation for test environments&lt;br&gt;
📡 powering future access points or cameras directly from the switch&lt;br&gt;
🛡️ network isolation for storage, monitoring or nodes&lt;br&gt;
📈 better performance analysis through advanced metrics&lt;/p&gt;

&lt;p&gt;For now&lt;br&gt;
simplicity wins&lt;br&gt;
but I want room to grow&lt;/p&gt;


&lt;h2&gt;
  
  
  🍓 The Brain of the LAN
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Raspberry Pi as DNS Server
&lt;/h3&gt;

&lt;p&gt;DNS is a critical piece in any infrastructure&lt;br&gt;
especially Kubernetes&lt;/p&gt;

&lt;p&gt;The Raspberry Pi handles:&lt;br&gt;
✅ local DNS resolution&lt;br&gt;
✅ service discovery across the home network&lt;br&gt;
✅ potential Pi hole extension later&lt;/p&gt;

&lt;p&gt;It ensures &lt;strong&gt;zero reliance on public DNS&lt;/strong&gt; for internal services&lt;br&gt;
Fast&lt;br&gt;
lightweight&lt;br&gt;
and rock solid 🧩&lt;/p&gt;


&lt;h2&gt;
  
  
  📦 The Data Layer
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Synology NAS for Storage and Backups
&lt;/h3&gt;

&lt;p&gt;Applications need persistent data&lt;br&gt;
databases&lt;br&gt;
monitoring stack&lt;br&gt;
GitOps metadata&lt;/p&gt;

&lt;p&gt;The Synology NAS provides:&lt;br&gt;
✅ NFS shares for general storage&lt;br&gt;
✅ iSCSI volumes for testing block level workloads&lt;br&gt;
✅ snapshots and RAID protection&lt;br&gt;
✅ backup space for Proxmox VMs&lt;/p&gt;

&lt;p&gt;It is the &lt;strong&gt;data backbone&lt;/strong&gt; of the homelab&lt;br&gt;
keeping stateful workloads safe 🧡&lt;/p&gt;


&lt;h2&gt;
  
  
  🔌 Hardware Architecture Diagram
&lt;/h2&gt;

&lt;p&gt;Here is a more complete view of the hardware layout&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                Internet
                    │
                 Router
               with VPN
                    │
             ┌──────┴──────┐
             │             │
       Gigabit Switch      │
    (Flat network today    │
     but future Managed PoE)
             │
 ┌───────────┼───────────────┬───────────┐
 │           │               │           │
[Node 1]  [Node 2]        [Node 3]   Synology NAS
Proxmox   Proxmox         Proxmox         
 │         │               │              
 └─────────┴───────────┬───┘              
                       │                  
                Raspberry Pi (DNS)        
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything works together as &lt;strong&gt;a single coherent platform&lt;/strong&gt;&lt;br&gt;
Compute + Networking + DNS + Storage&lt;br&gt;
self hosted and fully controlled ✅&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Why this hardware matters
&lt;/h2&gt;

&lt;p&gt;Every piece of this hardware plays a key role&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hardware&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lenovo Tiny Nodes&lt;/td&gt;
&lt;td&gt;Compute layer for Kubernetes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gigabit Switch&lt;/td&gt;
&lt;td&gt;Fast and reliable local communications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed PoE Switch future&lt;/td&gt;
&lt;td&gt;Network control and segmentation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raspberry Pi&lt;/td&gt;
&lt;td&gt;DNS authority for all internal traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Synology NAS&lt;/td&gt;
&lt;td&gt;Persistent data and VM backup storage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This creates a &lt;strong&gt;production style environment&lt;/strong&gt;&lt;br&gt;
ready for real workloads and real failures&lt;br&gt;
without cloud overhead or noise pollution&lt;/p&gt;

&lt;p&gt;A smart and scalable foundation 🤝&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Up Next
&lt;/h2&gt;

&lt;p&gt;Next article in the series&lt;br&gt;
➡️ Turning this hardware into a Proxmox cluster&lt;br&gt;
➡️ VM management&lt;br&gt;
➡️ Template strategy&lt;br&gt;
➡️ Solid virtualization layer for Kubernetes&lt;/p&gt;

&lt;p&gt;Stay tuned&lt;br&gt;
The fun is only beginning 😎&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cloud</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title>🏡 Why I Built My Own Homelab to Run Kubernetes</title>
      <dc:creator>Hamdi (KHELIL) LION</dc:creator>
      <pubDate>Sun, 26 Oct 2025 20:10:21 +0000</pubDate>
      <link>https://dev.to/hkhelil/why-i-built-my-own-homelab-to-run-kubernetes-ke9</link>
      <guid>https://dev.to/hkhelil/why-i-built-my-own-homelab-to-run-kubernetes-ke9</guid>
      <description>&lt;p&gt;Hey everyone 👋&lt;/p&gt;

&lt;p&gt;If you have been following my posts, you might have noticed that I have been pretty quiet lately 😅&lt;br&gt;
There is a good reason for that&lt;/p&gt;

&lt;p&gt;I have been spending a lot of time building my own homelab a place where I can safely test, break, and rebuild complex infrastructures without worrying about cloud costs or production risks 🧑‍💻&lt;/p&gt;

&lt;p&gt;My goal is simple&lt;br&gt;
Create a production grade Kubernetes environment at home using Proxmox for virtualization and Kubespray for cluster provisioning 💡&lt;br&gt;
This environment allows me to test real world scenarios and also experiment with features for my Kubermates project 🚀&lt;/p&gt;

&lt;p&gt;Let me walk you through the why and the how&lt;/p&gt;




&lt;p&gt;⚙️ The Motivation&lt;/p&gt;

&lt;p&gt;As a DevOps and SRE engineer, Kubernetes is part of my daily life.&lt;br&gt;
But testing new tools, scaling strategies, GitOps workflows, or security rules often requires:&lt;/p&gt;

&lt;p&gt;✅ complete freedom&lt;br&gt;
✅ zero fear of downtime&lt;br&gt;
✅ repeatability&lt;br&gt;
✅ controlled costs&lt;/p&gt;

&lt;p&gt;A homelab gives me full control of the stack including: network, storage, virtualization, routing, DNS, automation and resilience strategies 💪&lt;/p&gt;

&lt;p&gt;It rapidly became much more than a hobby. It is now my mini data center&lt;/p&gt;




&lt;p&gt;🧩 Proxmox as the Foundation&lt;/p&gt;

&lt;p&gt;To host everything, I chose Proxmox VE&lt;/p&gt;

&lt;p&gt;Reasons behind this choice:&lt;br&gt;
🖥️ clean and powerful web UI and CLI&lt;br&gt;
⚙️ excellent virtualization and storage integration&lt;br&gt;
🌐 flexible networking and clustering&lt;br&gt;
🔁 snapshots and backup system made for tests and disasters&lt;/p&gt;

&lt;p&gt;Each hypervisor node runs multiple virtual machines that will become control planes, workers or utility nodes like DNS and monitoring services&lt;/p&gt;

&lt;p&gt;Proxmox gives me the freedom to rebuild everything as many times as needed&lt;/p&gt;




&lt;p&gt;☁️ Kubernetes Deployment With Kubespray&lt;/p&gt;

&lt;p&gt;Once the virtual machines were ready, I needed a repeatable approach to deploy Kubernetes.&lt;br&gt;
I selected Kubespray because:&lt;/p&gt;

&lt;p&gt;🔥 it is production proven&lt;br&gt;
🧩 uses Ansible which makes automation easy&lt;br&gt;
🔧 allows deep customization&lt;br&gt;
🔄 supports highly available clusters&lt;/p&gt;

&lt;p&gt;This lets me rebuild the entire cluster in minutes while keeping the configuration under version control&lt;/p&gt;

&lt;p&gt;Perfect for experimentation&lt;/p&gt;




&lt;p&gt;💪 Homelab Specs&lt;/p&gt;

&lt;p&gt;Right now, my homelab is running with:&lt;/p&gt;

&lt;p&gt;🧠 96 GB of RAM&lt;br&gt;
⚡ 36 vCPUs&lt;/p&gt;

&lt;p&gt;This gives enough headroom to simulate:&lt;/p&gt;

&lt;p&gt;Autoscaling and performance constraints&lt;/p&gt;

&lt;p&gt;Monitoring with Prometheus, Loki, Thanos etc&lt;/p&gt;

&lt;p&gt;GitOps flows with Argo CD and Helmfile&lt;/p&gt;

&lt;p&gt;Network isolation and multi tenancy&lt;/p&gt;

&lt;p&gt;Production security patterns&lt;/p&gt;

&lt;p&gt;Computing constraints actually make the optimization challenges even more interesting&lt;/p&gt;




&lt;p&gt;🔬 A Playground for Kubermates&lt;/p&gt;

&lt;p&gt;If you know me, you know I am working on Kubermates&lt;br&gt;
This homelab is now my real life testing ground for the platform&lt;/p&gt;

&lt;p&gt;Here I can:&lt;br&gt;
✅ test onboarding flows and automation patterns&lt;br&gt;
✅ simulate multi tenant setups&lt;br&gt;
✅ validate best practices for deploy and operate&lt;br&gt;
✅ try both cluster and application level policies&lt;/p&gt;

&lt;p&gt;Everything is reproducible and controlled&lt;br&gt;
Exactly what I need to test responsibly&lt;/p&gt;




&lt;p&gt;🌐 Secure Remote Access With WireGuard VPN&lt;/p&gt;

&lt;p&gt;Although everything runs at home, I still want to access the environment securely when I travel or work away from my desk&lt;/p&gt;

&lt;p&gt;For this, I use WireGuard VPN running on a small gateway VM&lt;/p&gt;

&lt;p&gt;Why WireGuard&lt;/p&gt;

&lt;p&gt;🔒 strong encryption&lt;/p&gt;

&lt;p&gt;⚡ ultra fast performance&lt;/p&gt;

&lt;p&gt;🧩 minimal configuration&lt;/p&gt;

&lt;p&gt;📱 clients for Linux, Mac, Windows, iOS and Android&lt;/p&gt;

&lt;p&gt;My access architecture:&lt;/p&gt;

&lt;p&gt;Only VPN traffic can reach the Proxmox cluster and Kubernetes nodes&lt;/p&gt;

&lt;p&gt;No ports are exposed directly to the internet&lt;/p&gt;

&lt;p&gt;Internal DNS and Ingress work exactly as inside the local network&lt;/p&gt;

&lt;p&gt;So I get full access to Grafana, Argo CD, dashboards, kubectl and SSH&lt;br&gt;
as if I was physically at home 🏠&lt;/p&gt;

&lt;p&gt;This keeps everything private and secure&lt;br&gt;
Perfect for a personal lab&lt;/p&gt;




&lt;p&gt;🧠 Key Lessons Learned So Far&lt;/p&gt;

&lt;p&gt;Building Kubernetes at home gives you a deeper understanding of what is behind the cloud curtain&lt;/p&gt;

&lt;p&gt;Main takeaways&lt;/p&gt;

&lt;p&gt;Networking is always trickier than expected 😅&lt;/p&gt;

&lt;p&gt;Local DNS matters more than anyone admits&lt;/p&gt;

&lt;p&gt;Good VM templates save hours of setup time&lt;/p&gt;

&lt;p&gt;Resource constraints teach real FinOps awareness&lt;/p&gt;

&lt;p&gt;Breaking things makes you learn faster&lt;/p&gt;

&lt;p&gt;And the best part&lt;br&gt;
It is incredibly fun&lt;/p&gt;




&lt;p&gt;🚀 What Comes Next&lt;/p&gt;

&lt;p&gt;This is just the beginning&lt;/p&gt;

&lt;p&gt;Upcoming articles in the series&lt;br&gt;
1️⃣ Proxmox hardware and virtualization best practices&lt;br&gt;
2️⃣ Kubespray automation and cluster bootstrapping&lt;br&gt;
3️⃣ DNS and networking architecture for homelabs&lt;br&gt;
4️⃣ GitOps, certificates and observability stack deployment&lt;br&gt;
5️⃣ How to simulate production workloads at home&lt;/p&gt;

&lt;p&gt;The goal&lt;br&gt;
Help anyone build a professional grade Kubernetes experience locally&lt;br&gt;
without cloud billing surprises&lt;/p&gt;

&lt;p&gt;Thanks for reading and stay tuned for more 🔥&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>automation</category>
      <category>cloud</category>
    </item>
    <item>
      <title>🧩 GitHub Actions Composite vs Reusable Workflows</title>
      <dc:creator>Hamdi (KHELIL) LION</dc:creator>
      <pubDate>Fri, 18 Jul 2025 08:41:53 +0000</pubDate>
      <link>https://dev.to/hkhelil/github-actions-composite-vs-reusable-workflows-4bih</link>
      <guid>https://dev.to/hkhelil/github-actions-composite-vs-reusable-workflows-4bih</guid>
      <description>&lt;h2&gt;
  
  
  How to standardize and supercharge your CI/CD pipelines across projects
&lt;/h2&gt;

&lt;p&gt;When your teams manage multiple projects with similar deployment patterns, repeating the same GitHub Actions steps over and over can become tedious, error-prone, and hard to maintain&lt;/p&gt;

&lt;p&gt;Thankfully, GitHub Actions offers two powerful solutions to help &lt;strong&gt;standardize, reuse, and scale your CI/CD pipelines&lt;/strong&gt;: &lt;strong&gt;Composite Actions&lt;/strong&gt; and &lt;strong&gt;Reusable Workflows&lt;/strong&gt;. When used together, they form a clean, modular, and DRY (don’t repeat yourself) CI/CD strategy&lt;/p&gt;

&lt;h2&gt;
  
  
  🔄 Composite Actions vs Reusable Workflows
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🧱 Composite Actions
&lt;/h3&gt;

&lt;p&gt;Composite Actions allow you to group steps (like &lt;code&gt;docker build&lt;/code&gt;, &lt;code&gt;terraform plan&lt;/code&gt;, or &lt;code&gt;trivy scan&lt;/code&gt;) into a reusable component. Think of it like a function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reusing logic (e.g., build and push images)&lt;/li&gt;
&lt;li&gt;Hiding complex logic from the main workflow&lt;/li&gt;
&lt;li&gt;Keeping workflows minimal and maintainable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/actions/your-action&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🔁 Reusable Workflows
&lt;/h3&gt;

&lt;p&gt;Reusable Workflows are full workflows that can be invoked with inputs and secrets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High-level CI/CD orchestration (build, test, scan, deploy)&lt;/li&gt;
&lt;li&gt;Enforcing common practices across repositories&lt;/li&gt;
&lt;li&gt;Abstracting full pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;org/repo/.github/workflows/your-workflow.yml@ref&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🐳 Example: Docker Build and Scan with Trivy
&lt;/h2&gt;

&lt;p&gt;Here is an example of a &lt;strong&gt;composite action&lt;/strong&gt; to build and push a Docker image to Azure Container Registry (ACR) and scan it using Trivy.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;.github/actions/build-and-scan/action.yml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Docker Build and Push&lt;/span&gt;

&lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
  &lt;span class="na"&gt;image_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;build_args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
  &lt;span class="na"&gt;report_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trivy-report&lt;/span&gt;

&lt;span class="na"&gt;runs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;using&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;composite"&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@v2&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;creds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.AZURE_CREDENTIALS }}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/docker-login@v2&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;login-server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.CONTAINER_REGISTRY }}&lt;/span&gt;
        &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ACR_USERNAME }}&lt;/span&gt;
        &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ACR_PASSWORD }}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and Push Image&lt;/span&gt;
      &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;docker build ${{ inputs.build_args }} \&lt;/span&gt;
          &lt;span class="s"&gt;-t ${{ env.CONTAINER_REGISTRY }}/${{ inputs.image_name }}:${{ inputs.tag }} \&lt;/span&gt;
          &lt;span class="s"&gt;-f "${{ inputs.dockerfile }}" "${{ inputs.context }}"&lt;/span&gt;
        &lt;span class="s"&gt;docker push ${{ env.CONTAINER_REGISTRY }}/${{ inputs.image_name }}:${{ inputs.tag }}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Scan with Trivy&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasecurity/trivy-action@0.32.0&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image-ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;env.CONTAINER_REGISTRY&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.image_name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}:${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.tag&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sarif'&lt;/span&gt;
        &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;trivy-results.sarif'&lt;/span&gt;
        &lt;span class="na"&gt;exit-code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload SARIF to GitHub Security tab&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github/codeql-action/upload-sarif@v3&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;sarif_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;trivy-results.sarif'&lt;/span&gt;
        &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trivy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Usage in Workflow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and Scan&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.github/actions/build-and-scan&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app&lt;/span&gt;
    &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.sha }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🌍 Example: Terraform/Terragrunt Validation with Checkov
&lt;/h2&gt;

&lt;p&gt;Now let’s see a &lt;strong&gt;reusable workflow&lt;/strong&gt; to validate Terraform code and scan for misconfigurations using Checkov.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;.github/workflows/infra-check.yml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Infra Validation Workflow&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_call&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Terragrunt and OpenTofu&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -s https://raw.githubusercontent.com/opentofu/opentofu/main/scripts/install.sh | bash&lt;/span&gt;
          &lt;span class="s"&gt;curl -L https://github.com/gruntwork-io/terragrunt/releases/latest/download/terragrunt_linux_amd64 -o terragrunt&lt;/span&gt;
          &lt;span class="s"&gt;chmod +x terragrunt&lt;/span&gt;
          &lt;span class="s"&gt;sudo mv terragrunt /usr/local/bin/&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Validate HCL Syntax&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;terragrunt hclfmt --terragrunt-check --terragrunt-diff --recursive&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.working_dir }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check Misconfigurations with Checkov&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridgecrewio/checkov-action@v12&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.working_dir }}&lt;/span&gt;
          &lt;span class="na"&gt;quiet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Usage in Project Workflow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;call-validation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-org/your-repo/.github/workflows/infra-check.yml@main&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;infrastructure/staging/eu-west-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ⚙️ Best Practices
&lt;/h2&gt;

&lt;p&gt;✅ Keep composite actions in &lt;code&gt;.github/actions&lt;/code&gt;&lt;br&gt;
✅ Keep reusable workflows in &lt;code&gt;.github/workflows&lt;/code&gt;&lt;br&gt;
✅ Abstract frequently repeated patterns&lt;br&gt;
✅ Use inputs, outputs, and env vars to increase flexibility&lt;br&gt;
✅ Use &lt;code&gt;workflow_call&lt;/code&gt; and &lt;code&gt;workflow_dispatch&lt;/code&gt; to structure triggers&lt;br&gt;
✅ Defer secrets to the calling workflow&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 Conclusion
&lt;/h2&gt;

&lt;p&gt;Using &lt;strong&gt;composite actions&lt;/strong&gt; and &lt;strong&gt;reusable workflows&lt;/strong&gt; together can transform your CI/CD processes into clean, repeatable, and secure pipelines. Whether you're building containers, provisioning infrastructure, or scanning for vulnerabilities, this modular approach will scale with your teams and your systems.&lt;/p&gt;

&lt;p&gt;Enjoy building better pipelines !&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>github</category>
      <category>cicd</category>
      <category>automation</category>
    </item>
    <item>
      <title>🚀🌐 Elevating Infrastructure: From Terraform/Terragrunt Foundations to Platform Engineering 😊</title>
      <dc:creator>Hamdi (KHELIL) LION</dc:creator>
      <pubDate>Fri, 18 Apr 2025 10:24:24 +0000</pubDate>
      <link>https://dev.to/hkhelil/elevating-infrastructure-from-terraformterragrunt-foundations-to-platform-engineering-41fc</link>
      <guid>https://dev.to/hkhelil/elevating-infrastructure-from-terraformterragrunt-foundations-to-platform-engineering-41fc</guid>
      <description>&lt;p&gt;Hey there, cloud adventurers! 🚀 Let’s chat about why keeping &lt;strong&gt;Terraform&lt;/strong&gt; (or &lt;strong&gt;OpenTofu&lt;/strong&gt;) and &lt;strong&gt;Terragrunt&lt;/strong&gt; in their own lanes is absolutely essential—and how using &lt;strong&gt;Terraform JSON tfvars&lt;/strong&gt; makes life easier when you’re building nifty tools on top. Ready? Let’s dive in! 😄&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Separation Is a Must, Not an Option 🙅‍♂️🙅‍♀️
&lt;/h3&gt;

&lt;p&gt;It might be tempting to mix Terraform and Terragrunt into one big file—after all, they work together, right? But trust me, keeping them decoupled is a game‑changer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🔒 Clear boundaries&lt;/strong&gt;: Terraform focuses purely on &lt;em&gt;resource definitions&lt;/em&gt;, while Terragrunt handles &lt;em&gt;orchestration&lt;/em&gt;, remote state, and DRY patterns.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔄 Independent versioning&lt;/strong&gt;: You can upgrade your Terraform (or OpenTofu) modules (semantic versioning FTW!) without touching your Terragrunt configs—and vice versa!
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📦 Reusability everywhere&lt;/strong&gt;: Modules stay generic and shareable across projects when they don’t carry Terragrunt baggage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By giving each tool its own space, you simplify CI/CD, make upgrades safer, and keep responsibilities crystal‑clear. Win‑win! 🏆&lt;/p&gt;

&lt;h3&gt;
  
  
  Module Versioning with Terraform 🎯
&lt;/h3&gt;

&lt;p&gt;Hosting Terraform (or OpenTofu) modules in a versioned repo (GitHub, GitLab, private registry—you choose!) lets you tag releases like &lt;code&gt;v1.2.3&lt;/code&gt;. Then, Terragrunt can lock to that exact version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/storage-account/main.tf&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_storage_account"&lt;/span&gt; &lt;span class="s2"&gt;"sa"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account_name&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_group_name&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;account_tier&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Standard"&lt;/span&gt;
  &lt;span class="nx"&gt;account_replication_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LRS"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"account_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"resource_group_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"location"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// modules/storage-account/versions.tf&lt;/span&gt;
&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;azurerm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/azurerm"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 3.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;required_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 1.5"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Publishing this as, say,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;git&lt;/span&gt;&lt;span class="err"&gt;::&lt;/span&gt;&lt;span class="nx"&gt;ssh&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//git@github.com/myorg/terraform-modules-repo.git//storage-account?ref=v1.0.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;means everyone downstream knows exactly what they’re getting—no surprises! 🎉&lt;/p&gt;

&lt;h3&gt;
  
  
  Decoupling Terraform (or OpenTofu) &amp;amp; Terragrunt 🛠️
&lt;/h3&gt;

&lt;p&gt;For true decoupling, &lt;strong&gt;host your Terraform/OpenTofu modules in a separate Git repository&lt;/strong&gt;, each with its own lifecycle, version tags, and release process. Then reference those modules from Terragrunt using Git sources. This keeps module development and environment orchestration fully independent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example Terragrunt config:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# live/dev/terragrunt.hcl&lt;/span&gt;
&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"git::ssh://git@github.com/myorg/terraform-modules-repo.git//storage-account?ref=v1.0.0"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;inputs&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsondecode&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"${get_terragrunt_dir()}/variables.tfvars.json"&lt;/span&gt;&lt;span class="err"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The modules repository evolves on its own schedule.
&lt;/li&gt;
&lt;li&gt;Terragrunt configs in &lt;code&gt;live/&lt;/code&gt; reference specific module versions via Git source.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pattern prevents accidental cross‑pollination of concerns, making upgrades, reviews, and rollbacks a breeze. ✨&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Terraform JSON &lt;code&gt;tfvars&lt;/code&gt; Rocks 📑✨
&lt;/h3&gt;

&lt;p&gt;Sure, HCL is human‑friendly, but &lt;strong&gt;JSON tfvars&lt;/strong&gt; shine when you need to build tools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;🤖 Machine‑readable&lt;/strong&gt;: Every language can parse JSON out of the box—no extra libraries needed!
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;✔️ Schema validation&lt;/strong&gt;: Hook up a JSON Schema to catch mistakes before you even hit “apply.”
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;💡 Dynamic generation&lt;/strong&gt;: Your self‑service portal or CLI can spin out a JSON file in milliseconds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Example &lt;code&gt;variables.tfvars.json&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"account_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"myappstorage-${env}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource_group_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rg-${env}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eastus"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"platform-team"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And Terragrunt gobbles it up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# live/dev/terragrunt.hcl&lt;/span&gt;
&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"git::ssh://git@github.com/myorg/terraform-modules-repo.git//storage-account?ref=v1.0.0"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;inputs&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsondecode&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"${get_terragrunt_dir()}/variables.tfvars.json"&lt;/span&gt;&lt;span class="err"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See how clean that is? 🥳&lt;/p&gt;

&lt;h3&gt;
  
  
  Building Your Own Abstraction Tools 🧰
&lt;/h3&gt;

&lt;p&gt;Looking to give your team a self‑service experience? Here are two solid approaches:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. CLI with Go &amp;amp; Cobra ⚙️&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast scaffolding&lt;/strong&gt;: Cobra generates a project structure with commands, subcommands, and flag parsing out of the box.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type safety &amp;amp; performance&lt;/strong&gt;: Go’s static typing ensures reliable flag handling, and compiled binaries run blazingly fast.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin architecture&lt;/strong&gt;: Easily hook in custom modules—for example, a command like &lt;code&gt;infra gen&lt;/code&gt; that generates Terraform JSON tfvars files for your Terragrunt inputs.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Example snippet:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;  &lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"encoding/json"&lt;/span&gt;
    &lt;span class="s"&gt;"io/ioutil"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/spf13/cobra"&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;accountName&lt;/span&gt;       &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;resourceGroupName&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;location&lt;/span&gt;          &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;env&lt;/span&gt;               &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;owner&lt;/span&gt;             &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;varsFile&lt;/span&gt;          &lt;span class="kt"&gt;string&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rootCmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cobra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Use&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;"infra"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Short&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Self-service infra CLI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;genCmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cobra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Use&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;"gen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Short&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Generate Terraform tfvars JSON file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RunE&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cobra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
        &lt;span class="s"&gt;"account_name"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;accountName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"resource_group_name"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resourceGroupName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"location"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;            &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"tags"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="s"&gt;"environment"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s"&gt;"owner"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MarshalIndent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"  "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ioutil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;varsFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0644&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;genCmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Flags&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;accountName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"account-name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Azure storage account name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;genCmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Flags&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;resourceGroupName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"resource-group"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Resource group name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;genCmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Flags&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"eastus"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Azure location"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;genCmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Flags&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Deployment environment"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;genCmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Flags&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"platform-team"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Resource owner"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;genCmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Flags&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;varsFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"out"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"variables.tfvars.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Output JSON tfvars file"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rootCmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;genCmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Self‑service portal with Backstage 🎭&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unified developer portal&lt;/strong&gt;: Backstage by Spotify lets you surface docs, tooling, and pipelines in one UI.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure as Code plugin&lt;/strong&gt;: Create a custom Backstage plugin to list available Terraform/OpenTofu modules and trigger Terragrunt runs with JSON inputs.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy &amp;amp; approval workflows&lt;/strong&gt;: Integrate with existing Identity/Access tools and GitOps pipelines for reviews before provisioning.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rich catalog&lt;/strong&gt;: Use Backstage’s software catalog to track module versions, show metadata (owner, tags, docs), and link to Git repos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combine these approaches with strict Terraform/Terragrunt separation and JSON tfvars, and you’ll empower your team with both CLI efficiency and a polished web portal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrapping Up 🎁
&lt;/h3&gt;

&lt;p&gt;Separating Terraform (or OpenTofu) modules from Terragrunt configs isn’t just a “nice to have”—it’s &lt;em&gt;essential&lt;/em&gt; for maintainable, scalable infrastructure. Pair that with JSON tfvars, and you’re set to build amazing tooling that your whole team will love. Happy provisioning! 🌟&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>terraform</category>
      <category>cloudnative</category>
      <category>terragrunt</category>
    </item>
    <item>
      <title>🔐 Secure Secret Management with SOPS in Helm 🚀</title>
      <dc:creator>Hamdi (KHELIL) LION</dc:creator>
      <pubDate>Thu, 27 Feb 2025 08:15:10 +0000</pubDate>
      <link>https://dev.to/hkhelil/secure-secret-management-with-sops-in-helm-1940</link>
      <guid>https://dev.to/hkhelil/secure-secret-management-with-sops-in-helm-1940</guid>
      <description>&lt;p&gt;When managing applications deployed on Kubernetes, keeping secrets safe while still making them accessible to Helm charts is a challenge. Storing secrets in plaintext is a &lt;strong&gt;security risk&lt;/strong&gt; 🚨 — and that’s where &lt;strong&gt;SOPS&lt;/strong&gt; (Secrets OPerationS) and the &lt;strong&gt;Helm Secrets plugin&lt;/strong&gt; come in!&lt;/p&gt;

&lt;p&gt;In this guide, we’ll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ How to use &lt;strong&gt;SOPS&lt;/strong&gt; with &lt;strong&gt;age&lt;/strong&gt; and &lt;strong&gt;GPG&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ How to configure &lt;strong&gt;SOPS with &lt;code&gt;sops.yaml&lt;/code&gt;&lt;/strong&gt; for better management&lt;/li&gt;
&lt;li&gt;✅ How to use &lt;strong&gt;Helm Secrets Plugin&lt;/strong&gt; to manage encrypted secrets directly in your Helm charts&lt;/li&gt;
&lt;li&gt;✅ A &lt;strong&gt;GitHub Actions workflow&lt;/strong&gt; to securely deploy Helm charts using encrypted secrets&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📌 Why Use SOPS with Helm?
&lt;/h2&gt;

&lt;p&gt;SOPS is an open-source tool from Mozilla that lets you &lt;strong&gt;encrypt and decrypt&lt;/strong&gt; secrets with ease. When combined with the Helm Secrets plugin, you can safely store your sensitive data in Git repositories and automatically decrypt them during Helm deployments. Here’s why it’s awesome:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Keeps secrets encrypted in your repos
&lt;/li&gt;
&lt;li&gt;✅ Works with YAML, JSON, and ENV files
&lt;/li&gt;
&lt;li&gt;✅ Integrates seamlessly with Helm via the &lt;strong&gt;Helm Secrets plugin&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ Fits perfectly into CI/CD pipelines like &lt;strong&gt;GitHub Actions&lt;/strong&gt; for secure deployments&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🔑 Using SOPS with &lt;code&gt;age&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/FiloSottile/age" rel="noopener noreferrer"&gt;Age&lt;/a&gt; is a modern, simple, and secure encryption tool. If you’re new to encryption, &lt;strong&gt;age is a great alternative to GPG&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✨ Step 1: Install &lt;code&gt;age&lt;/code&gt; and &lt;code&gt;sops&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Install &lt;code&gt;age&lt;/code&gt; and &lt;code&gt;sops&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;age    &lt;span class="c"&gt;# Ubuntu/Debian&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✨ Step 2: Generate an &lt;code&gt;age&lt;/code&gt; Key
&lt;/h3&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;age-keygen &lt;span class="nt"&gt;-o&lt;/span&gt; ~/.config/sops/age/keys.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will generate a key similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# public key: age1xxxxxxx
AGE-SECRET-KEY-1XXXXXXYYYYYYYYZZZZZZ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the &lt;strong&gt;public key&lt;/strong&gt; (&lt;code&gt;age1xxxxxxx&lt;/code&gt;)—this will be used for encryption.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✨ Step 3: Encrypt a YAML File with SOPS
&lt;/h3&gt;

&lt;p&gt;Create a file called &lt;code&gt;secrets.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;db_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin"&lt;/span&gt;
&lt;span class="na"&gt;db_password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;supersecret"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, encrypt it using SOPS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sops &lt;span class="nt"&gt;--encrypt&lt;/span&gt; &lt;span class="nt"&gt;--age&lt;/span&gt; age1xxxxxxx &lt;span class="nt"&gt;-i&lt;/span&gt; secrets.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you open &lt;code&gt;secrets.yaml&lt;/code&gt;, you’ll see it’s fully encrypted! 🛡️&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;decrypt&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sops &lt;span class="nt"&gt;--decrypt&lt;/span&gt; secrets.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔧 Configuring &lt;code&gt;sops.yaml&lt;/code&gt; for Better Management
&lt;/h2&gt;

&lt;p&gt;Instead of specifying the encryption method manually every time, SOPS supports a configuration file (&lt;code&gt;.sops.yaml&lt;/code&gt;). This makes it easier to manage secrets across your team.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;.sops.yaml&lt;/code&gt; in your repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;creation_rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path_regex&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secrets/.*\.yaml$&lt;/span&gt;
    &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;age1xxxxxxx&lt;/span&gt;  &lt;span class="c1"&gt;# Replace with your public key&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path_regex&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secrets/.*\.json$&lt;/span&gt;
    &lt;span class="na"&gt;pgp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ABC12345&lt;/span&gt;  &lt;span class="c1"&gt;# Replace with your GPG key ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when encrypting secrets inside the &lt;code&gt;secrets/&lt;/code&gt; folder, SOPS will &lt;strong&gt;automatically&lt;/strong&gt; use the right encryption method! 🎉&lt;/p&gt;

&lt;p&gt;Encrypt a new secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sops &lt;span class="nt"&gt;--encrypt&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; secrets/app.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🛠️ Using Helm with the Helm Secrets Plugin
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Helm Secrets plugin&lt;/strong&gt; allows you to work with encrypted secrets directly in your Helm charts—no need to expose sensitive data!&lt;/p&gt;

&lt;h3&gt;
  
  
  ✨ Step 1: Install the Helm Secrets Plugin
&lt;/h3&gt;

&lt;p&gt;Install the plugin with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm plugin &lt;span class="nb"&gt;install &lt;/span&gt;https://github.com/jkroepke/helm-secrets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This plugin leverages SOPS to decrypt your secret files during Helm chart deployments.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✨ Step 2: Encrypt Your Secrets File
&lt;/h3&gt;

&lt;p&gt;Create a file named &lt;code&gt;secrets.yaml&lt;/code&gt; (if you haven’t already):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-secret&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Opaque&lt;/span&gt;
&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YWRtaW4=&lt;/span&gt;          &lt;span class="c1"&gt;# base64 encoded&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;c3VwZXJzZWNyZXQ=&lt;/span&gt;   &lt;span class="c1"&gt;# base64 encoded&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Encrypt it using SOPS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sops &lt;span class="nt"&gt;--encrypt&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; secrets.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✨ Step 3: Deploy with Helm Using Encrypted Secrets
&lt;/h3&gt;

&lt;p&gt;Deploy your Helm chart using the encrypted secrets file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm secrets upgrade &lt;span class="nt"&gt;--install&lt;/span&gt; my-release ./my-chart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Helm Secrets plugin will automatically decrypt &lt;code&gt;secrets.yaml&lt;/code&gt; during the deployment process. 🚀&lt;/p&gt;

&lt;h2&gt;
  
  
  🤖 Using SOPS and Helm in GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Integrate your secure secrets management into your CI/CD pipeline with GitHub Actions. Here’s an example workflow that deploys your Helm chart with encrypted secrets:&lt;/p&gt;

&lt;h3&gt;
  
  
  ✨ Step 1: Store the &lt;code&gt;age&lt;/code&gt; Private Key in GitHub Secrets
&lt;/h3&gt;

&lt;p&gt;In your GitHub repository, navigate to &lt;strong&gt;Settings → Secrets and variables → Actions&lt;/strong&gt;, and add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SOPS_AGE_KEY&lt;/code&gt;: The &lt;strong&gt;private key&lt;/strong&gt; from &lt;code&gt;~/.config/sops/age/keys.txt&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ✨ Step 2: Create the GitHub Actions Workflow
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy with Helm &amp;amp; SOPS&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout repository&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;sudo apt-get update&lt;/span&gt;
          &lt;span class="s"&gt;sudo apt-get install -y sops age&lt;/span&gt;
          &lt;span class="s"&gt;curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash&lt;/span&gt;
          &lt;span class="s"&gt;helm plugin install https://github.com/jkroepke/helm-secrets&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up SOPS&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p ~/.config/sops/age/&lt;/span&gt;
          &lt;span class="s"&gt;echo "${{ secrets.SOPS_AGE_KEY }}" &amp;gt; ~/.config/sops/age/keys.txt&lt;/span&gt;
          &lt;span class="s"&gt;chmod 600 ~/.config/sops/age/keys.txt&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy with Helm&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;helm secrets upgrade --install my-release ./my-chart&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🔥 What Happens in This Workflow?
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Checks out the code&lt;/strong&gt; ✅
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Installs SOPS, age, Helm, and the Helm Secrets plugin&lt;/strong&gt; ✅
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loads the age private key from GitHub Secrets&lt;/strong&gt; ✅
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploys the Helm chart&lt;/strong&gt; with decrypted secrets on the fly ✅
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Security Tip:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Make sure that any decrypted files are never committed to your repository! Always keep them out of version control. 🔒&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 Wrapping Up
&lt;/h2&gt;

&lt;p&gt;SOPS and the Helm Secrets plugin offer a &lt;strong&gt;powerful&lt;/strong&gt; and &lt;strong&gt;secure&lt;/strong&gt; way to manage secrets in your Kubernetes deployments. With &lt;strong&gt;age&lt;/strong&gt; encryption, a handy &lt;code&gt;.sops.yaml&lt;/code&gt; configuration, and seamless integration via Helm, managing secrets has never been easier! 💪&lt;/p&gt;

&lt;p&gt;By integrating these tools into your workflow, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Encrypted secrets&lt;/strong&gt; safely stored in Git repositories
&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Automatic decryption&lt;/strong&gt; during Helm deployments
&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Secure usage&lt;/strong&gt; of secrets in CI/CD pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Want to take it a step further? Try exploring &lt;strong&gt;AWS KMS, GCP KMS, or Azure Key Vault&lt;/strong&gt; for even tighter security! 🔐🚀&lt;/p&gt;

&lt;p&gt;Have questions or suggestions? Drop them in the comments! 💬&lt;/p&gt;

&lt;p&gt;Happy clustering and stay safe! 🔐😊&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
