<?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: Adeoye Malumi</title>
    <description>The latest articles on DEV Community by Adeoye Malumi (@oyebobs).</description>
    <link>https://dev.to/oyebobs</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%2F1436441%2Faa047a2b-d84d-4a76-9c89-5082c6029c97.jpeg</url>
      <title>DEV Community: Adeoye Malumi</title>
      <link>https://dev.to/oyebobs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oyebobs"/>
    <language>en</language>
    <item>
      <title>Kubernetes Network Policies Explained (and How I Actually Made Them Work)</title>
      <dc:creator>Adeoye Malumi</dc:creator>
      <pubDate>Thu, 09 Jul 2026 16:50:22 +0000</pubDate>
      <link>https://dev.to/oyebobs/kubernetes-network-policies-explained-and-how-i-actually-made-them-work-4bpd</link>
      <guid>https://dev.to/oyebobs/kubernetes-network-policies-explained-and-how-i-actually-made-them-work-4bpd</guid>
      <description>&lt;p&gt;If you've ever wondered, "How do I stop my pods from randomly talking to everything in my cluster?" – this is the post for you.&lt;/p&gt;

&lt;p&gt;Today, I dove into &lt;strong&gt;Kubernetes Network Policies&lt;/strong&gt; – the way you control who can talk to whom inside your cluster. I started with &lt;code&gt;weavenet&lt;/code&gt;, then switched to &lt;strong&gt;Calico&lt;/strong&gt;, and I'm excited to walk you through how it all came together.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Even Is a Network Policy?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;NetworkPolicy&lt;/strong&gt; in Kubernetes is like a firewall rule for your pods. It lets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Control &lt;strong&gt;inbound traffic&lt;/strong&gt; (who can talk to a pod)&lt;/li&gt;
&lt;li&gt;Control &lt;strong&gt;outbound traffic&lt;/strong&gt; (where a pod can talk to)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can create a &lt;strong&gt;deny-all&lt;/strong&gt; policy that blocks all incoming traffic to a pod.&lt;/li&gt;
&lt;li&gt;Or an &lt;strong&gt;allow-only&lt;/strong&gt; policy that says: "Only the &lt;code&gt;backend&lt;/code&gt; pods can talk to the &lt;code&gt;mysql&lt;/code&gt; pod on port 3306."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without network policies, by default, &lt;strong&gt;everything can talk to everything&lt;/strong&gt; in your cluster. That's fine for demos, terrible for real security.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Needed a Different CNI
&lt;/h2&gt;

&lt;p&gt;Kubernetes itself doesn't implement network policies. It just defines them. The actual work is done by your &lt;strong&gt;CNI plugin&lt;/strong&gt; (Container Network Interface).&lt;/p&gt;

&lt;p&gt;I started with &lt;strong&gt;weavenet&lt;/strong&gt;, which supports basic network policies. But as I dug deeper, I realized I wanted something more powerful and production-friendly. That's when I switched to &lt;strong&gt;Calico&lt;/strong&gt;, which is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fast&lt;/li&gt;
&lt;li&gt;Scalable&lt;/li&gt;
&lt;li&gt;Great for complex network policies&lt;/li&gt;
&lt;li&gt;Works well with &lt;code&gt;kind&lt;/code&gt; clusters (as long as you configure it correctly)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're just experimenting, weavenet is fine. If you want to level up your cluster networking, Calico is a great next step.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Test Cluster: A Kind Setup with Calico
&lt;/h2&gt;

&lt;p&gt;To test network policies, I created a simple &lt;code&gt;kind&lt;/code&gt; cluster with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 control-plane node&lt;/li&gt;
&lt;li&gt;2 worker nodes&lt;/li&gt;
&lt;li&gt;Disabled default CNI (&lt;code&gt;disableDefaultCNI: true&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Custom pod subnet: &lt;code&gt;192.168.0.0/16&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  kind Cluster YAML
&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;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cluster&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;kind.x-k8s.io/v1alpha4&lt;/span&gt;
&lt;span class="na"&gt;nodes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;control-plane&lt;/span&gt;
    &lt;span class="na"&gt;extraPortMappings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;30001&lt;/span&gt;
        &lt;span class="na"&gt;hostPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30001&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worker&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;role&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;networking&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;disableDefaultCNI&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;podSubnet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;192.168.0.0/16"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating the Cluster
&lt;/h3&gt;

&lt;p&gt;With the YAML saved as &lt;code&gt;kind-config.yaml&lt;/code&gt;, I created the cluster like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kind create cluster &lt;span class="nt"&gt;--name&lt;/span&gt; day26 &lt;span class="nt"&gt;--config&lt;/span&gt; kind-config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I checked the nodes:&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 nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the part that was a bit confusing at first:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Until I installed a CNI (like Calico), the worker nodes stayed in a &lt;code&gt;NotReady&lt;/code&gt; state.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's because Kubernetes needs a CNI to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assign IPs to pods&lt;/li&gt;
&lt;li&gt;Set up networking between nodes&lt;/li&gt;
&lt;li&gt;Enable traffic to flow across the cluster&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without a CNI, the control-plane node can be &lt;code&gt;Ready&lt;/code&gt;, but the worker nodes will remain &lt;code&gt;NotReady&lt;/code&gt; until networking is configured.&lt;/p&gt;

&lt;p&gt;Only after installing Calico did all nodes show as &lt;code&gt;Ready&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 get nodes
&lt;span class="c"&gt;# NAME           STATUS   ROLES           AGE   VERSION&lt;/span&gt;
&lt;span class="c"&gt;# day26-control-plane   Ready    control-plane   ...&lt;/span&gt;
&lt;span class="c"&gt;# day26-worker          Ready    worker          ...&lt;/span&gt;
&lt;span class="c"&gt;# day26-worker2         Ready    worker          ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once Calico was running, my cluster was ready for real network policy experiments.&lt;/p&gt;




&lt;h2&gt;
  
  
  The App I Built: Frontend → Backend → MySQL
&lt;/h2&gt;

&lt;p&gt;To make network policies meaningful, I created a tiny 3-tier app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;frontend&lt;/code&gt; pod + service&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;backend&lt;/code&gt; pod + service&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mysql&lt;/code&gt; pod + service (&lt;code&gt;db&lt;/code&gt; service)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pods &amp;amp; Services
&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;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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;frontend&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;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;frontend&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;nginx&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;nginx&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;80&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="err"&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;frontend&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;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;frontend&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;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;frontend&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;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;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="m"&gt;80&lt;/span&gt;
&lt;span class="err"&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;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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend&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;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend&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;nginx&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;nginx&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;80&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="err"&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;backend&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;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend&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;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend&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;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;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="m"&gt;80&lt;/span&gt;
&lt;span class="err"&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;db&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;mysql&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;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;mysql&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;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;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3306&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3306&lt;/span&gt;
&lt;span class="err"&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;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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql&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;mysql&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;mysql&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;mysql:latest&lt;/span&gt;
      &lt;span class="na"&gt;env&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;MYSQL_USER"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mysql"&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;MYSQL_PASSWORD"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mysql"&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;MYSQL_DATABASE"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;testdb"&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;MYSQL_ROOT_PASSWORD"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verysecure"&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;3306&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;At this point, &lt;strong&gt;everything could talk to everything&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;frontend&lt;/code&gt; could reach &lt;code&gt;backend&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;backend&lt;/code&gt; could reach &lt;code&gt;mysql&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;frontend&lt;/code&gt; could also reach &lt;code&gt;mysql&lt;/code&gt; directly (which we probably don't want)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now it was time to lock it down.&lt;/p&gt;




&lt;h2&gt;
  
  
  My First Real Network Policy: Only Backend → MySQL
&lt;/h2&gt;

&lt;p&gt;The goal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mysql&lt;/code&gt; should &lt;strong&gt;only&lt;/strong&gt; accept traffic from pods with label &lt;code&gt;role: backend&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;On port &lt;strong&gt;3306&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;No one else (including &lt;code&gt;frontend&lt;/code&gt;) should touch the database&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  NetworkPolicy Manifest
&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;networking.k8s.io/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;NetworkPolicy&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;db-test&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;podSelector&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;mysql&lt;/span&gt;
  &lt;span class="na"&gt;policyTypes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
  &lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;podSelector&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;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend&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;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3306&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;podSelector&lt;/code&gt;: targets the &lt;code&gt;mysql&lt;/code&gt; pod (&lt;code&gt;name: mysql&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;policyTypes: [Ingress]&lt;/code&gt;: we're controlling inbound traffic&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ingress.from.podSelector&lt;/code&gt;: only pods with &lt;code&gt;role: backend&lt;/code&gt; are allowed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ports&lt;/code&gt;: only port 3306&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After applying this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;backend&lt;/code&gt; → &lt;code&gt;mysql&lt;/code&gt;  works&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;frontend&lt;/code&gt; → &lt;code&gt;mysql&lt;/code&gt;  blocked&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mysql&lt;/code&gt; → anywhere (outbound) still allowed (we didn't restrict Egress)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How I Tested It
&lt;/h2&gt;

&lt;p&gt;I used a simple approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run a temporary pod with &lt;code&gt;curl&lt;/code&gt; or &lt;code&gt;mysql&lt;/code&gt; client.&lt;/li&gt;
&lt;li&gt;Try to reach services from different pods.&lt;/li&gt;
&lt;li&gt;Observe which connections succeed or fail.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example:&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;# From frontend pod – should fail&lt;/span&gt;
kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; frontend &lt;span class="nt"&gt;--&lt;/span&gt; curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://db:3306

&lt;span class="c"&gt;# From backend pod – should succeed&lt;/span&gt;
kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; backend &lt;span class="nt"&gt;--&lt;/span&gt; curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://db:3306
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seeing the first one fail and the second one succeed was the "aha!" moment: &lt;strong&gt;the policy is actually working&lt;/strong&gt;.&lt;/p&gt;




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

&lt;p&gt;Network Policies are one of those things that feel abstract until you actually apply them and see connections blocked in real time. Once that happens, you suddenly understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why security matters&lt;/li&gt;
&lt;li&gt;Why "everything can talk to everything" is not a strategy&lt;/li&gt;
&lt;li&gt;How Kubernetes gives you granular control over your cluster's traffic&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>learning</category>
      <category>kubernetes</category>
      <category>cloudnative</category>
      <category>devops</category>
    </item>
    <item>
      <title>Understanding Kubernetes Service Accounts (The Identity Every Pod Uses)</title>
      <dc:creator>Adeoye Malumi</dc:creator>
      <pubDate>Tue, 07 Jul 2026 15:58:47 +0000</pubDate>
      <link>https://dev.to/oyebobs/understanding-kubernetes-service-accounts-the-identity-every-pod-uses-4lcd</link>
      <guid>https://dev.to/oyebobs/understanding-kubernetes-service-accounts-the-identity-every-pod-uses-4lcd</guid>
      <description>&lt;p&gt;Most of us learn about Kubernetes users before we learn about Service Accounts. That's actually where the confusion starts Service Accounts end up looking like "just another kind of user," and you file them away without really getting what makes them different.&lt;/p&gt;

&lt;p&gt;They're not just another user. They solve a completely different problem.&lt;/p&gt;

&lt;p&gt;Today I learned that human users authenticate &lt;em&gt;from outside&lt;/em&gt; the cluster — you, sitting at your laptop, running &lt;code&gt;kubectl&lt;/code&gt; with your certificate or your login. But Pods run &lt;em&gt;inside&lt;/em&gt; the cluster, and they often need to talk to the Kubernetes API too — to list other pods, read a ConfigMap, watch for changes, whatever. A Pod can't exactly type in a username and password. It needs its own identity.&lt;/p&gt;

&lt;p&gt;That identity is called a &lt;strong&gt;Service Account&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  User vs. Service Account
&lt;/h2&gt;

&lt;p&gt;Before touching a single command, it's worth seeing the two side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;User&lt;/th&gt;
&lt;th&gt;Service Account&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Used by humans&lt;/td&gt;
&lt;td&gt;Used by Pods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External to Kubernetes&lt;/td&gt;
&lt;td&gt;Managed by Kubernetes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usually authenticated using certificates&lt;/td&gt;
&lt;td&gt;Authenticated using tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You create it outside the cluster&lt;/td&gt;
&lt;td&gt;Kubernetes creates and manages it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keep that table in your head — everything below is really just this table playing out in the terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Checking Existing Service Accounts
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;❯ k get sa
NAME      AGE
default   38d
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice I didn't create this. It was already there, 38 days old — as old as the namespace itself.&lt;/p&gt;

&lt;p&gt;That's the first thing to internalize: &lt;strong&gt;every namespace automatically gets a Service Account called &lt;code&gt;default&lt;/code&gt;.&lt;/strong&gt; You don't ask for it. If you create a Pod and never say which Service Account it should use, Kubernetes quietly attaches this one for you. Keep that in mind — it comes back to bite (in a good way) later in this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Creating My Own Service Account
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;❯ k create sa build-sa
serviceaccount/build-sa created

❯ k describe sa build-sa
Name:                build-sa
Namespace:           default
&lt;/span&gt;&lt;span class="gp"&gt;Labels:              &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;Annotations:         &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;Image pull secrets:  &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;Events:              &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple enough — I now have a second identity, &lt;code&gt;build-sa&lt;/code&gt;, sitting in the &lt;code&gt;default&lt;/code&gt; namespace, ready to be attached to Pods later.&lt;/p&gt;

&lt;p&gt;But here's the question that trips a lot of beginners up: &lt;strong&gt;why doesn't it have a token?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you learned Kubernetes a few years ago, or you're reading an older tutorial, you'll expect &lt;code&gt;describe sa&lt;/code&gt; to show a &lt;code&gt;Tokens:&lt;/code&gt; field pointing at an auto-generated Secret. Not anymore. Newer versions of Kubernetes stopped automatically minting long-lived Secret-based tokens for every Service Account, mainly for security reasons — a token that never expires and just sits around as a Secret is a standing risk if it leaks. Instead, Kubernetes now prefers short-lived, automatically-rotated tokens issued on demand.&lt;/p&gt;

&lt;p&gt;Since I specifically wanted a durable token I could inspect and use for &lt;code&gt;--as&lt;/code&gt; testing, I had to create it myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Creating a ServiceAccount Token Secret
&lt;/h2&gt;

&lt;p&gt;I wrote a &lt;code&gt;secret.yaml&lt;/code&gt; that, at minimum, needs this annotation:&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;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&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;kubernetes.io/service-account.name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build-sa&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one line is doing all the work. It tells the Kubernetes controller: &lt;em&gt;"This Secret isn't just a random Opaque blob — it's a token for the &lt;code&gt;build-sa&lt;/code&gt; Service Account."&lt;/em&gt; Because of that annotation, a built-in controller notices the Secret, generates a signed token for &lt;code&gt;build-sa&lt;/code&gt;, and populates the Secret with it automatically.&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;❯ k apply -f secret.yaml
secret/build-robot-secret created

❯ k get secret
NAME                 TYPE                                  DATA   AGE
backend-user         Opaque                                1      5d3h
build-robot-secret   kubernetes.io/service-account-token   3      14s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the &lt;code&gt;TYPE&lt;/code&gt; column — &lt;code&gt;kubernetes.io/service-account-token&lt;/code&gt;, not &lt;code&gt;Opaque&lt;/code&gt;. That type is exactly what tells Kubernetes "treat this Secret specially and fill it with SA credentials."&lt;/p&gt;

&lt;h3&gt;
  
  
  Inspecting the Secret
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;❯ k describe secret build-robot-secret
Name:         build-robot-secret
Namespace:    default
&lt;/span&gt;&lt;span class="gp"&gt;Labels:       &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;Annotations:  kubernetes.io/service-account.name: build-sa
              kubernetes.io/service-account.uid: d065d358-58fa-44a6-9656-8263e0fa05eb

Type:  kubernetes.io/service-account-token

Data
====
ca.crt:     1107 bytes
namespace:  7 bytes
token:      eyJhbGciOiJSUzI1NiIs... (truncated)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three fields, each with a job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ca.crt&lt;/code&gt;&lt;/strong&gt; — the cluster's certificate authority. Whoever holds this Secret can verify they're really talking to the real API server, not an impersonator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;namespace&lt;/code&gt;&lt;/strong&gt; — just the namespace this identity belongs to (&lt;code&gt;default&lt;/code&gt;), so any client using this Secret knows where it "lives."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;token&lt;/code&gt;&lt;/strong&gt; — the important one. This is a &lt;strong&gt;JWT (JSON Web Token)&lt;/strong&gt;, signed by the cluster. This is what a Pod, script, or client hands to the API server to say "I am &lt;code&gt;build-sa&lt;/code&gt;, and here's proof." I've truncated the token above — treat these things like passwords, not like log output, even in a lab cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you decode a Service Account JWT (there's nothing secret about the &lt;em&gt;structure&lt;/em&gt;, just don't paste your actual token online), you'll find claims like the issuer, the namespace, and a &lt;code&gt;sub&lt;/code&gt; field that looks like &lt;code&gt;system:serviceaccount:default:build-sa&lt;/code&gt;. That string is the Service Account's real, canonical identity inside Kubernetes — remember it, it matters in a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Permissions
&lt;/h2&gt;

&lt;p&gt;Now the interesting part. I wanted to see what &lt;code&gt;build-sa&lt;/code&gt; could actually do, without creating a Pod yet:&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;❯ k get pods --as build-sa
Error from server (Forbidden): pods is forbidden: User "build-sa" cannot list resource "pods" in API group "" in the namespace "default"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to unpack here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First — what does &lt;code&gt;--as&lt;/code&gt; actually do?&lt;/strong&gt; It's easy to assume you're "logging in" as &lt;code&gt;build-sa&lt;/code&gt;. You're not. &lt;code&gt;--as&lt;/code&gt; is &lt;strong&gt;impersonation&lt;/strong&gt;. You're still using &lt;em&gt;your own&lt;/em&gt; admin credentials to talk to the API server, but you're telling it: "Evaluate this request as if it came from &lt;code&gt;build-sa&lt;/code&gt; instead of me." The API server checks whether &lt;em&gt;your&lt;/em&gt; account is allowed to impersonate &lt;code&gt;build-sa&lt;/code&gt; (admin usually is), and if so, it re-runs the authorization check as that identity. It's a testing and debugging superpower — you get to try on someone else's permissions without actually needing their credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second — why Forbidden?&lt;/strong&gt; This is the single most important lesson in this whole exercise: &lt;strong&gt;the Service Account existed. The token existed. Authentication succeeded. Authorization failed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those are two separate gates in Kubernetes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; — "Who are you?" &lt;code&gt;build-sa&lt;/code&gt; proved this fine, via impersonation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization&lt;/strong&gt; — "What are you allowed to do?" Nothing had granted &lt;code&gt;build-sa&lt;/code&gt; any permissions at all, so RBAC said no.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A lot of beginners collapse these into one concept. They're not the same thing, and separating them in your head will save you hours of confused debugging later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the Role
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;❯ k create role build-role \
--verb=list,get,watch \
--resource=pod
role.rbac.authorization.k8s.io/build-role created
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking down the flags:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;role build-role&lt;/code&gt; — I'm creating a &lt;strong&gt;Role&lt;/strong&gt;, which is a namespaced set of permissions (as opposed to a &lt;strong&gt;ClusterRole&lt;/strong&gt;, which applies cluster-wide).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--verb=list,get,watch&lt;/code&gt; — the specific actions this Role allows. Not &lt;code&gt;create&lt;/code&gt;, not &lt;code&gt;delete&lt;/code&gt; — just read-style operations.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--resource=pod&lt;/code&gt; — the Kubernetes resource type these verbs apply to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On its own, a Role does nothing. It's a permission &lt;em&gt;template&lt;/em&gt; sitting unused until something binds an identity to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the RoleBinding
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;❯ k create rolebinding rb \
--role=build-role \
--user=build-sa
rolebinding.rbac.authorization.k8s.io/rb created
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the glue: it binds the &lt;code&gt;build-role&lt;/code&gt; permissions to the identity &lt;code&gt;build-sa&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One honest caveat worth flagging, since I want this post to be technically accurate and not just "it worked so it's fine": I used &lt;code&gt;--user=build-sa&lt;/code&gt; here, and it worked in this exercise because I was testing purely through impersonation with &lt;code&gt;--as build-sa&lt;/code&gt;, and &lt;code&gt;--as&lt;/code&gt; treats whatever string you give it as a plain username.&lt;/p&gt;

&lt;p&gt;But that's not actually how a real Service Account identifies itself to the cluster. Its canonical identity is &lt;code&gt;system:serviceaccount:&amp;lt;namespace&amp;gt;:&amp;lt;name&amp;gt;&lt;/code&gt; — in my case, &lt;code&gt;system:serviceaccount:default:build-sa&lt;/code&gt;. In production, the standard, correct way to bind a RoleBinding to a Service Account is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--serviceaccount=default:build-sa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I'd actually mounted this token inside a real client and tried to use it (rather than testing through admin impersonation), a binding made with &lt;code&gt;--user=build-sa&lt;/code&gt; would &lt;strong&gt;not&lt;/strong&gt; match that Service Account's real identity, and the request would still be Forbidden. So: my command worked for this specific impersonation test, but if you're setting this up for a real workload, use &lt;code&gt;--serviceaccount=&amp;lt;namespace&amp;gt;:&amp;lt;name&amp;gt;&lt;/code&gt; instead. Good to know the difference exists before it costs you a debugging session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Again
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;❯ k get pods --as build-sa
No resources found in default namespace.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No error this time — just an empty result, because there genuinely were no pods yet. Authentication &lt;em&gt;and&lt;/em&gt; authorization both passed. That's the fix landing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a Deployment
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;❯ k create deploy test --image=nginx
deployment.apps/test created
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth pointing out deliberately: I created this Deployment as &lt;em&gt;myself&lt;/em&gt; (admin), not as &lt;code&gt;build-sa&lt;/code&gt;. &lt;code&gt;build-sa&lt;/code&gt; only has &lt;code&gt;list&lt;/code&gt;, &lt;code&gt;get&lt;/code&gt;, and &lt;code&gt;watch&lt;/code&gt; on pods — nowhere near enough permission to create a Deployment. If I'd tried &lt;code&gt;k create deploy test --image=nginx --as build-sa&lt;/code&gt;, it would have been Forbidden too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching the Pod
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;❯ k get po -w --as build-sa
NAME                    READY   STATUS    RESTARTS   AGE
test-56848fd9dc-fhdjv   1/1     Running   0          89s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-w&lt;/code&gt; flag means &lt;strong&gt;watch&lt;/strong&gt; — instead of printing the pod state once and exiting, &lt;code&gt;kubectl&lt;/code&gt; keeps the connection open and streams updates as the pod's status changes. Combined with &lt;code&gt;--as build-sa&lt;/code&gt;, this also confirms &lt;code&gt;build-sa&lt;/code&gt;'s &lt;code&gt;watch&lt;/code&gt; verb from the Role is working, not just &lt;code&gt;list&lt;/code&gt; and &lt;code&gt;get&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going Inside the Pod
&lt;/h2&gt;

&lt;p&gt;This is where it got genuinely interesting.&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;❯ k exec -it test-56848fd9dc-fhdjv bash
error: exec [POD] [COMMAND] is not supported anymore. Use exec [POD] -- [COMMAND] instead
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hit this error a few times before it clicked. Older &lt;code&gt;kubectl&lt;/code&gt; versions let you run &lt;code&gt;kubectl exec pod bash&lt;/code&gt; and it would figure out where your flags ended and the container's command began. Modern &lt;code&gt;kubectl&lt;/code&gt; refuses to guess — it wants an explicit &lt;code&gt;--&lt;/code&gt; to separate "arguments meant for kubectl" from "the command to run inside the container." So:&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;❯ k exec -it test-56848fd9dc-fhdjv -- bash
&lt;/span&gt;&lt;span class="gp"&gt;root@test-56848fd9dc-fhdjv:/#&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That extra &lt;code&gt;--&lt;/code&gt; is the whole fix. Everything after it belongs to the container, not to &lt;code&gt;kubectl&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Most Interesting Discovery
&lt;/h2&gt;

&lt;p&gt;Once inside, I went looking for exactly the kind of thing this Pod would need to talk to the API server — and there it was:&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="gp"&gt;root@test-56848fd9dc-fhdjv:/var/run/secrets/kubernetes.io/serviceaccount#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lrt&lt;/span&gt;
&lt;span class="go"&gt;total 0
&lt;/span&gt;&lt;span class="gp"&gt;lrwxrwxrwx 1 root root 12 Jul  7 14:38 token -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;..data/token
&lt;span class="gp"&gt;lrwxrwxrwx 1 root root 16 Jul  7 14:38 namespace -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;..data/namespace
&lt;span class="gp"&gt;lrwxrwxrwx 1 root root 13 Jul  7 14:38 ca.crt -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;..data/ca.crt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same three files as the Secret I inspected earlier — &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;namespace&lt;/code&gt;, &lt;code&gt;ca.crt&lt;/code&gt; — automatically mounted at &lt;code&gt;/var/run/secrets/kubernetes.io/serviceaccount&lt;/code&gt;, with &lt;strong&gt;no configuration from me at all.&lt;/strong&gt; Kubernetes does this for every Pod by default, via a projected volume, so that any process running inside the container can immediately authenticate to the API server if it needs to (this is how things like controllers and operators, running as Pods themselves, talk back to Kubernetes).&lt;/p&gt;

&lt;p&gt;Now the obvious beginner question, and the one I asked myself immediately: &lt;strong&gt;"Wait — I created &lt;code&gt;build-sa&lt;/code&gt;. Why does the token in here say &lt;code&gt;default&lt;/code&gt;?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sure enough, decoding what's inside shows a &lt;code&gt;sub&lt;/code&gt; claim of &lt;code&gt;system:serviceaccount:default:default&lt;/code&gt; — the &lt;code&gt;default&lt;/code&gt; Service Account, not &lt;code&gt;build-sa&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The answer is almost anticlimactic once you see it: my Deployment spec never included &lt;code&gt;serviceAccountName: build-sa&lt;/code&gt;. I never told Kubernetes which Service Account this Pod should use — so it fell back to the one thing every namespace already has, the one from Step 1. &lt;strong&gt;The &lt;code&gt;default&lt;/code&gt; Service Account.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is, genuinely, the most valuable lesson from the whole exercise: creating a Service Account doesn't automatically attach it to anything. You have to explicitly wire it into a Pod (or Deployment, or Job, etc.) with &lt;code&gt;serviceAccountName&lt;/code&gt; — or Kubernetes will silently keep using &lt;code&gt;default&lt;/code&gt; on your behalf, and you won't get an error telling you. You'll just quietly have the wrong permissions, or, worse in production, the &lt;em&gt;default&lt;/em&gt; Service Account might have more access than you meant to grant that workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Service Accounts are identities for Pods — not humans.&lt;/li&gt;
&lt;li&gt;Every namespace automatically gets a &lt;code&gt;default&lt;/code&gt; Service Account.&lt;/li&gt;
&lt;li&gt;Service Accounts authenticate using tokens (JWTs), not certificates.&lt;/li&gt;
&lt;li&gt;Newer Kubernetes versions don't auto-create long-lived token Secrets anymore — you create them explicitly when you need one.&lt;/li&gt;
&lt;li&gt;RBAC still governs what a Service Account is allowed to do, exactly like it does for users.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--as&lt;/code&gt; impersonates another identity for testing — it doesn't log you in as them.&lt;/li&gt;
&lt;li&gt;Authentication and authorization are two separate checks: proving who you are is not the same as being allowed to do something.&lt;/li&gt;
&lt;li&gt;Bind RoleBindings to Service Accounts with &lt;code&gt;--serviceaccount=&amp;lt;namespace&amp;gt;:&amp;lt;name&amp;gt;&lt;/code&gt; in real setups, not &lt;code&gt;--user=&amp;lt;name&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Pods automatically get their Service Account's credentials mounted at &lt;code&gt;/var/run/secrets/kubernetes.io/serviceaccount&lt;/code&gt; — no extra config needed.&lt;/li&gt;
&lt;li&gt;If you don't set &lt;code&gt;serviceAccountName&lt;/code&gt; on a Pod, Kubernetes silently uses &lt;code&gt;default&lt;/code&gt;. Always check.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>learning</category>
      <category>cloudcomputing</category>
    </item>
    <item>
      <title>Understanding Kubernetes ClusterRoles and ClusterRoleBindings</title>
      <dc:creator>Adeoye Malumi</dc:creator>
      <pubDate>Tue, 07 Jul 2026 13:13:42 +0000</pubDate>
      <link>https://dev.to/oyebobs/understanding-kubernetes-clusterroles-and-clusterrolebindings-1k64</link>
      <guid>https://dev.to/oyebobs/understanding-kubernetes-clusterroles-and-clusterrolebindings-1k64</guid>
      <description>&lt;p&gt;Yesterday I learned about Roles and RoleBindings. Today I explored ClusterRoles and ClusterRoleBindings for cluster-scoped resources like Nodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Role Wasn't Enough
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl auth can-i get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warning: resource 'nodes' is not namespace scoped
no
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nodes are cluster-scoped resources, so a namespace Role cannot grant access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a ClusterRole
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create clusterrole node-reader &lt;span class="nt"&gt;--verb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;get,list,watch &lt;span class="nt"&gt;--resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&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 clusterrole | &lt;span class="nb"&gt;grep &lt;/span&gt;node-reader
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Creating a ClusterRoleBinding
&lt;/h2&gt;

&lt;p&gt;I first made a mistake:&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 clusterrolebinding reader-binding &lt;span class="nt"&gt;--clusterrole&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cluster-reader &lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;adeoye
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl auth can-i get nodes &lt;span class="nt"&gt;--as&lt;/span&gt; adeoye
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;no - RBAC: clusterrole.rbac.authorization.k8s.io "cluster-reader" not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The binding referenced a ClusterRole that didn't exist.&lt;/p&gt;

&lt;p&gt;I fixed it by deleting and recreating the binding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete clusterrolebinding reader-binding
kubectl create clusterrolebinding reader-binding &lt;span class="nt"&gt;--clusterrole&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;node-reader &lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;adeoye
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl auth can-i get nodes &lt;span class="nt"&gt;--as&lt;/span&gt; adeoye
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;returned:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing as adeoye
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl config use-context adeoye
kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Listing nodes worked.&lt;/p&gt;

&lt;p&gt;Describing a node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe node cka-cluster3-worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;showed most information, but RBAC blocked access to &lt;strong&gt;leases&lt;/strong&gt; and &lt;strong&gt;pods&lt;/strong&gt; because the ClusterRole only granted access to &lt;strong&gt;nodes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Trying to delete a node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete node cka-cluster3-worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;returned &lt;strong&gt;Forbidden&lt;/strong&gt; because the ClusterRole only included &lt;code&gt;get&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;, and &lt;code&gt;watch&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Roles are namespace-scoped.&lt;/li&gt;
&lt;li&gt;ClusterRoles are cluster-scoped.&lt;/li&gt;
&lt;li&gt;ClusterRoleBindings grant cluster-wide permissions.&lt;/li&gt;
&lt;li&gt;RBAC permissions are resource-specific.&lt;/li&gt;
&lt;li&gt;Kubernetes follows the principle of least privilege.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>learning</category>
      <category>devops</category>
      <category>kubernetes</category>
      <category>cloudcomputing</category>
    </item>
    <item>
      <title>Understanding Kubernetes RBAC: Roles, RoleBindings, and and Client Certificates</title>
      <dc:creator>Adeoye Malumi</dc:creator>
      <pubDate>Mon, 06 Jul 2026 14:14:46 +0000</pubDate>
      <link>https://dev.to/oyebobs/understanding-kubernetes-rbac-roles-rolebindings-and-and-client-certificates-d97</link>
      <guid>https://dev.to/oyebobs/understanding-kubernetes-rbac-roles-rolebindings-and-and-client-certificates-d97</guid>
      <description>&lt;p&gt;One thing I discovered today is that before Kubernetes can decide &lt;strong&gt;what you're allowed to do&lt;/strong&gt;, it first needs to know &lt;strong&gt;who you are&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That means there are two separate steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; – Proving your identity using a client certificate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization&lt;/strong&gt; – Deciding what you're allowed to do using RBAC.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this lab, I created my own Kubernetes user named &lt;strong&gt;adeoye&lt;/strong&gt;, authenticated the user with a certificate, and then used RBAC to control what that user could access.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 – Generate a Private Key
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; adeoye.key 2048
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates a 2048-bit RSA private key.&lt;/p&gt;

&lt;p&gt;Think of this as your digital identity. It should never be shared.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2 – Generate a Certificate Signing Request (CSR)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; adeoye.key &lt;span class="nt"&gt;-out&lt;/span&gt; adeoye.csr &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=adeoye"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Certificate Signing Request asks Kubernetes to issue a client certificate.&lt;/p&gt;

&lt;p&gt;The important part is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/CN=adeoye
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Common Name (CN) becomes the Kubernetes username.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 – Base64 Encode the CSR
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;adeoye.csr | &lt;span class="nb"&gt;base64&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes expects the CSR to be embedded inside a YAML manifest as Base64-encoded text. The &lt;code&gt;tr -d '\n'&lt;/code&gt; removes line breaks so it becomes a single continuous string.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4 – Submit the CSR
&lt;/h2&gt;

&lt;p&gt;After inserting the encoded CSR into &lt;code&gt;csr.yaml&lt;/code&gt;, I submitted 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; csr.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initially, the request was in the &lt;strong&gt;Pending&lt;/strong&gt; state so I had to approve the request.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5 – Approve the Certificate
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl certificate approve adeoye
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Approving the request tells Kubernetes to trust the user and issue a signed client certificate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6 – Extract the Signed Certificate
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get csr adeoye &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.certificate}'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; adeoye.crt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;adeoye.key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;adeoye.crt&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key proves my identity, while the certificate proves Kubernetes trusts that identity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7 – Configure kubectl
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl config set-credentials adeoye &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--client-key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;adeoye.key &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--client-certificate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;adeoye.crt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I created a context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl config set-context adeoye &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kind-cka-cluster3 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;adeoye
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A mistake I made:&lt;/strong&gt; I initially typed &lt;code&gt;lind-cka-cluster3&lt;/code&gt; instead of &lt;code&gt;kind-cka-cluster3&lt;/code&gt;, which caused API errors until I corrected the cluster name.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Finally, I switched to my new user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl config use-context adeoye
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point I was authenticated, but I still didn't have permissions to perform actions.&lt;/p&gt;




&lt;h2&gt;
  
  
  RBAC Time!
&lt;/h2&gt;

&lt;p&gt;Authentication tells Kubernetes &lt;strong&gt;who I am&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;RBAC tells Kubernetes &lt;strong&gt;what I'm allowed to do&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the Role
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;role.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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rbac.authorization.k8s.io/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;Role&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="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="s"&gt;pod-reader&lt;/span&gt;

&lt;span class="na"&gt;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;apiGroups&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;"&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="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pods"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;verbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;get&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;watch&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;list&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This Role only allows reading Pods.&lt;/p&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; role.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Creating the RoleBinding
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;binding.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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rbac.authorization.k8s.io/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;RoleBinding&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;read-pods&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;subjects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&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;User&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;adeoye&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;rbac.authorization.k8s.io&lt;/span&gt;

&lt;span class="na"&gt;roleRef&lt;/span&gt;&lt;span class="pi"&gt;:&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;Role&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;pod-reader&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;rbac.authorization.k8s.io&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This binds the &lt;strong&gt;adeoye&lt;/strong&gt; user to the &lt;strong&gt;pod-reader&lt;/strong&gt; Role.&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; binding.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Testing the Permissions
&lt;/h3&gt;

&lt;p&gt;Verify permission:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl auth can-i get pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;List 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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No resources found in default namespace.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means the command succeeded, but there were simply no Pods.&lt;/p&gt;

&lt;p&gt;Next, I tried listing Deployments.&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error from server (Forbidden): deployments.apps is forbidden
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why Could I Read Pods but Not Deployments?
&lt;/h2&gt;

&lt;p&gt;The answer is in the Role.&lt;/p&gt;

&lt;p&gt;It only grants access to:&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;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pods&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; include Deployments.&lt;/p&gt;

&lt;p&gt;Deployments belong to the &lt;strong&gt;apps&lt;/strong&gt; API group, while Pods belong to the core API group.&lt;/p&gt;

&lt;p&gt;Since my RoleBinding connects &lt;strong&gt;adeoye&lt;/strong&gt; to the &lt;strong&gt;pod-reader&lt;/strong&gt; Role, Kubernetes only allows the permissions defined there.&lt;/p&gt;

&lt;p&gt;No Deployment permissions were granted, so Kubernetes correctly denied access.&lt;/p&gt;

&lt;p&gt;This is the Principle of Least Privilege in action.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes first authenticates users using client certificates.&lt;/li&gt;
&lt;li&gt;The certificate's Common Name becomes the Kubernetes username.&lt;/li&gt;
&lt;li&gt;A Role defines permissions.&lt;/li&gt;
&lt;li&gt;A RoleBinding assigns those permissions to users.&lt;/li&gt;
&lt;li&gt;Authentication and authorization are separate processes.&lt;/li&gt;
&lt;li&gt;RBAC only grants explicitly defined permissions.&lt;/li&gt;
&lt;li&gt;Pods and Deployments are different resources, so access to one doesn't imply access to the other.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;Today's lab tied together authentication and authorization. Creating my own client certificate, configuring a new kubectl context, and then restricting that user with RBAC made the concepts much easier to understand.&lt;/p&gt;

&lt;p&gt;Security in Kubernetes isn't about giving users full access,but about giving them exactly the permissions they need, nothing more.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>cloud</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Day 20&amp;21/40 - Manage TLS Certificates In a Kubernetes Cluster</title>
      <dc:creator>Adeoye Malumi</dc:creator>
      <pubDate>Fri, 03 Jul 2026 11:57:11 +0000</pubDate>
      <link>https://dev.to/oyebobs/day-202140-manage-tls-certificates-in-a-kubernetes-cluster-27k0</link>
      <guid>https://dev.to/oyebobs/day-202140-manage-tls-certificates-in-a-kubernetes-cluster-27k0</guid>
      <description>&lt;p&gt;Today's challenge was about understanding how Kubernetes uses TLS certificates to secure communication between its components, and getting hands-on by onboarding a "new user" into the cluster using a Certificate Signing Request (CSR).&lt;/p&gt;

&lt;p&gt;If you've ever wondered how &lt;code&gt;kubectl&lt;/code&gt; actually proves to the API server that you're allowed to run commands, this is the exercise that answers it.&lt;/p&gt;

&lt;p&gt;A quick rule of thumb before diving in: &lt;strong&gt;if a filename or extension has the word "key" in it, it's a private key. Otherwise, it's a public certificate.&lt;/strong&gt; Keep that in mind as you read through the commands below.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 1: Generate a private key and a CSR
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; adeoye.key 2048
openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; adeoye.key &lt;span class="nt"&gt;-out&lt;/span&gt; adeoye.csr &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=adeoye"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;genrsa&lt;/code&gt; creates the private key, and &lt;code&gt;req -new&lt;/code&gt; uses that key to generate a Certificate Signing Request. The &lt;code&gt;-subj "/CN=adeoye"&lt;/code&gt; flag sets the "Common Name" on the certificate — this becomes the username Kubernetes will recognize once the cert is issued.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; I typed &lt;code&gt;adam.key&lt;/code&gt; in my &lt;code&gt;req&lt;/code&gt; command before actually creating a file with that name, and got:&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Could not open file or uri for loading private key from adam.key: No such file or directory
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Lesson learned — the key file has to exist &lt;em&gt;before&lt;/em&gt; you reference it when generating the CSR. Double-check your filenames match across commands.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 2: Base64-encode the CSR
&lt;/h3&gt;

&lt;p&gt;The CSR object in Kubernetes expects the request in base64 form inside a YAML manifest. This is where things got interesting:&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;cat &lt;/span&gt;adeoye.csr | &lt;span class="nb"&gt;base64 tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's wrong — it treats &lt;code&gt;tr&lt;/code&gt; as a filename argument for &lt;code&gt;base64&lt;/code&gt;, not a separate piped command:&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;base64&lt;/span&gt;: extra operand &lt;span class="s1"&gt;'/'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The correct version needs a pipe (&lt;code&gt;|&lt;/code&gt;) before &lt;code&gt;tr&lt;/code&gt;, and the actual newline character (&lt;code&gt;\n&lt;/code&gt;), not the literal string &lt;code&gt;"/n"&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;cat &lt;/span&gt;adeoye.csr | &lt;span class="nb"&gt;base64&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you a single-line base64 blob with no line breaks — exactly what the CSR YAML needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Create the CSR manifest and apply it
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;csr.yaml&lt;/code&gt; file with the base64-encoded request embedded in it, following the structure from the &lt;a href="https://kubernetes.io/docs/reference/access-authn-authz/certificate-signing-requests/#create-certificatessigningrequest" rel="noopener noreferrer"&gt;Kubernetes docs&lt;/a&gt;, then:&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; csr.yaml
kubectl get csr
&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     AGE   SIGNERNAME                            REQUESTOR          REQUESTEDDURATION   CONDITION
adeoye   25s   kubernetes.io/kube-apiserver-client   kubernetes-admin   24h                 Pending
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CSR shows up as &lt;code&gt;Pending&lt;/code&gt; — Kubernetes has received the request but it hasn't been approved yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Approve the CSR
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl certificate approve adeoye
&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;certificatesigningrequest.certificates.k8s.io/adeoye approved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking again with &lt;code&gt;kubectl get csr&lt;/code&gt; now shows &lt;code&gt;Approved,Issued&lt;/code&gt; — the cluster has signed the certificate.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; &lt;code&gt;kubectl approve certificate adeoye&lt;/code&gt; doesn't work — the order of the words matters:&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error: unknown command "approve" for "kubectl"
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;It's &lt;code&gt;kubectl certificate approve &amp;lt;name&amp;gt;&lt;/code&gt;, not &lt;code&gt;kubectl approve certificate &amp;lt;name&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 5: Retrieve and decode the issued certificate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get csr adeoye &lt;span class="nt"&gt;-o&lt;/span&gt; yaml &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; issuecert.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The issued certificate lives (base64-encoded) inside that YAML file under &lt;code&gt;status.certificate&lt;/code&gt;. To read the actual PEM certificate, decode it:&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;base64-blob&amp;gt;"&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prints out the human-readable certificate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-----BEGIN CERTIFICATE REQUEST-----
MIICVjCCAT4CAQAwETEPMA0GA1UEAww...
-----END CERTIFICATE REQUEST-----
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; Pasting a long base64 string directly into the terminal can trigger "bracketed paste mode," which shows up as weird escape codes and leaves your shell hanging on a &lt;code&gt;dquote&amp;gt;&lt;/code&gt; prompt. If that happens, press &lt;code&gt;Ctrl+C&lt;/code&gt; to cancel and try again — wrapping the string in quotes (&lt;code&gt;echo "..."&lt;/code&gt;) helps avoid it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To save the decoded certificate directly to a file instead of just printing it, redirect the output:&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;base64-blob&amp;gt;"&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; learner.crt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; Writing &lt;code&gt;-o learner.crt&lt;/code&gt; instead of &lt;code&gt;&amp;gt; learner.crt&lt;/code&gt; doesn't work here — &lt;code&gt;-o&lt;/code&gt; isn't a flag for &lt;code&gt;base64&lt;/code&gt;, and the shell will try to run it as its own command:&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;zsh: command not found: -o
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Use the &lt;code&gt;&amp;gt;&lt;/code&gt; redirect operator to send output to a file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Repeating the process for a second user
&lt;/h3&gt;

&lt;p&gt;To make sure the workflow actually sank in, I repeated the whole thing for a second user, &lt;code&gt;learner&lt;/code&gt;, this time being more careful with the pipe syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; learner.key 2048
openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; learner.key &lt;span class="nt"&gt;-out&lt;/span&gt; learner.csr &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=learner"&lt;/span&gt;
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; learner.yaml
kubectl certificate approve learner
kubectl get csr learner &lt;span class="nt"&gt;-o&lt;/span&gt; yaml &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; issuecert-learner.yaml
&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      AGE     SIGNERNAME                            REQUESTOR          REQUESTEDDURATION   CONDITION
learner   4m37s   kubernetes.io/kube-apiserver-client   kubernetes-admin   7d                  Approved,Issued
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the certificate was decoded and saved to &lt;code&gt;learner.crt&lt;/code&gt;, it would be ready to plug into a kubeconfig for that user, giving them client-cert-based access to the cluster.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key commands from today
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Generate a private key:&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;openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; adeoye.key 2048
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Generate a CSR:&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;openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; adeoye.key &lt;span class="nt"&gt;-out&lt;/span&gt; adeoye.csr &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=adeoye"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Approve a CSR:&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;kubectl certificate approve &amp;lt;certificate-signing-request-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Deny a CSR:&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;kubectl certificate deny &amp;lt;certificate-signing-request-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Reference:&lt;/strong&gt; &lt;a href="https://kubernetes.io/docs/reference/access-authn-authz/certificate-signing-requests/#create-certificatessigningrequest" rel="noopener noreferrer"&gt;Kubernetes docs — Create CertificateSigningRequest&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  What I took away from today
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A CSR workflow has four clean stages: generate a key → generate a CSR → submit it to the API server as a &lt;code&gt;CertificateSigningRequest&lt;/code&gt; object → get it approved and retrieve the signed cert.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;base64 | tr -d "\n"&lt;/code&gt; (with the pipe, and a real newline character) is the correct way to get a clean, single-line encoded string for a YAML manifest.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kubectl certificate approve&lt;/code&gt; — command, then subcommand, then name — not the other way around.&lt;/li&gt;
&lt;li&gt;Almost all my mistakes today were syntax slips (missing pipes, wrong flags, mismatched filenames), not conceptual ones. That's usually a good sign you understand &lt;em&gt;what&lt;/em&gt; you're doing, even when the terminal disagrees with you for a minute.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>tls</category>
      <category>cka</category>
    </item>
    <item>
      <title>Day 19/40 - Kubernetes ConfigMaps and Secrets</title>
      <dc:creator>Adeoye Malumi</dc:creator>
      <pubDate>Thu, 02 Jul 2026 11:22:28 +0000</pubDate>
      <link>https://dev.to/oyebobs/day-1940-kubernetes-configmaps-and-secrets-59ef</link>
      <guid>https://dev.to/oyebobs/day-1940-kubernetes-configmaps-and-secrets-59ef</guid>
      <description>&lt;p&gt;Today's mission: stop hardcoding values into pod manifests and learn how to inject them properly using &lt;strong&gt;ConfigMaps&lt;/strong&gt; and &lt;strong&gt;Secrets&lt;/strong&gt;. I also managed to break a few things along the way, which honestly taught me more than if everything had just worked first try. Let's get into it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is a ConfigMap?
&lt;/h2&gt;

&lt;p&gt;As your manifest grows, it becomes harder to manage a pile of environment variables directly inside it. A ConfigMap lets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pull those key-value pairs &lt;strong&gt;out of the manifest&lt;/strong&gt; and store them as their own Kubernetes object&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inject&lt;/strong&gt; that ConfigMap into a pod as environment variables (or as files, but that's for another day)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reuse&lt;/strong&gt; the same ConfigMap across multiple pods, so you're not repeating yourself everywhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Basically, it's a clean way to separate configuration from your application definition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a ConfigMap
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create cm app-cm &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;firstname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;adeoye &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;lastname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;malumi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;firstname&lt;/code&gt; and &lt;code&gt;lastname&lt;/code&gt; are the keys, and &lt;code&gt;adeoye&lt;/code&gt; / &lt;code&gt;malumi&lt;/code&gt; are the values.&lt;/p&gt;

&lt;h3&gt;
  
  
  My first "gotcha" of the day
&lt;/h3&gt;

&lt;p&gt;I actually tried to split this command across two lines like this:&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 cm app-cm &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;firstname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;adeoye &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;lastname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;malumi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First attempt gave me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error: exactly one NAME is required, got 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turns out I had a trailing space &lt;em&gt;after&lt;/em&gt; the backslash on the first line. In bash, the &lt;code&gt;\&lt;/code&gt; has to be the very last character on the line for line continuation to work. A space after it breaks the continuation, and the shell just treats the next line as a separate (garbled) argument. When I finally got the syntax right, &lt;code&gt;kubectl describe cm app-cm&lt;/code&gt; showed the value for &lt;code&gt;firstname&lt;/code&gt; as &lt;code&gt;adeoye--from-literal=lastname=malumi&lt;/code&gt; — my second flag had literally been glued onto the first value as text!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; either keep the command on one line, or make sure there's truly nothing (not even a space) after your &lt;code&gt;\&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once fixed, &lt;code&gt;describe&lt;/code&gt; showed exactly what I wanted:&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="s"&gt;Data&lt;/span&gt;
&lt;span class="s"&gt;====&lt;/span&gt;
&lt;span class="na"&gt;firstname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="s"&gt;----&lt;/span&gt;
&lt;span class="s"&gt;adeoye&lt;/span&gt;

&lt;span class="na"&gt;lastname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="s"&gt;----&lt;/span&gt;
&lt;span class="s"&gt;malumi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Injecting a ConfigMap into a Pod
&lt;/h2&gt;

&lt;p&gt;Here's the pod manifest I used to consume the ConfigMap as environment variables:&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;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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp-pod&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;app.kubernetes.io/name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MyApp&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;myapp-container&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:1.28&lt;/span&gt;
    &lt;span class="na"&gt;env&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;FIRSTNAME&lt;/span&gt;
      &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;configMapKeyRef&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;app-cm&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;firstname&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sh'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-c'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;echo&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;The&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;running!&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&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;3600'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;valueFrom.configMapKeyRef&lt;/code&gt; block tells Kubernetes: "don't hardcode this value — go fetch it from the &lt;code&gt;app-cm&lt;/code&gt; ConfigMap, using the &lt;code&gt;firstname&lt;/code&gt; key."&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha #2: Pods are mostly immutable
&lt;/h2&gt;

&lt;p&gt;After creating my ConfigMap with both &lt;code&gt;firstname&lt;/code&gt; and &lt;code&gt;lastname&lt;/code&gt;, I went back and added a &lt;code&gt;LASTNAME&lt;/code&gt; env var to my already-running pod's manifest, then ran:&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; pod.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And got slapped with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The Pod "myapp-pod" is invalid: spec: Forbidden: pod updates may not change fields other than 
`spec.containers[*].image`,`spec.initContainers[*].image`,`spec.activeDeadlineSeconds`,
`spec.tolerations` (only additions to existing tolerations),
`spec.terminationGracePeriodSeconds` (allow it to be set to 1 if it was previously negative)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was a great reminder: &lt;strong&gt;a bare Pod's spec is mostly frozen once it's created.&lt;/strong&gt; Kubernetes only lets you patch a small allowlist of fields in-place — image, a couple of grace period settings, and tolerations. Environment variables are &lt;em&gt;not&lt;/em&gt; on that list.&lt;/p&gt;

&lt;p&gt;Two ways around this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1 — Delete and recreate:&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;kubectl delete pod myapp-pod
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; pod.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option 2 — Force replace in one shot:&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;kubectl replace &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; pod.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I went with &lt;code&gt;replace --force&lt;/code&gt;, and it worked perfectly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ kubectl replace &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; pod.yaml
pod &lt;span class="s2"&gt;"myapp-pod"&lt;/span&gt; deleted from default namespace
pod/myapp-pod replaced
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verified inside the container:&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="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; myapp-pod &lt;span class="nt"&gt;--&lt;/span&gt; sh
/ &lt;span class="c"&gt;# echo $FIRSTNAME&lt;/span&gt;
adeoye
/ &lt;span class="c"&gt;# echo $LASTNAME&lt;/span&gt;
malumi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Side note:&lt;/strong&gt; this is actually one of the reasons Deployments exist and are preferred over bare Pods in real-world use. A Deployment lets you update the pod template (image, env vars, etc.) and &lt;code&gt;kubectl apply&lt;/code&gt; will handle the rolling replacement of pods for you — no manual delete/recreate dance required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Secrets: same idea, but for sensitive data
&lt;/h2&gt;

&lt;p&gt;Secrets work almost identically to ConfigMaps, except they're meant for sensitive values like passwords, tokens, or usernames you don't want sitting in plain text in your manifests.&lt;/p&gt;

&lt;p&gt;I followed the official Kubernetes docs on &lt;a href="https://kubernetes.io/docs/tasks/inject-data-application/distribute-credentials-secure/#define-container-environment-variables-using-secret-data" rel="noopener noreferrer"&gt;defining container environment variables using Secret data&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a Secret
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create secret generic backend-user &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;backend-username&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'adeoye-admin'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using the sample pod from the docs
&lt;/h3&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; https://k8s.io/examples/pods/inject/pod-single-secret-env-variable.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pod injects the secret as an environment variable called &lt;code&gt;SECRET_USERNAME&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  One more small hiccup
&lt;/h3&gt;

&lt;p&gt;Right after creating the pod, I tried to exec into it immediately:&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="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; env-single-secret &lt;span class="nt"&gt;--&lt;/span&gt; /bin/sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'echo $SECRET_USERNAME'&lt;/span&gt;
error: Internal error occurred: unable to upgrade connection: container not found &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"envars-test-container"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking &lt;code&gt;kubectl get po&lt;/code&gt; showed the pod was still &lt;code&gt;ContainerCreating&lt;/code&gt;. Classic case of being too impatient — I just needed to wait a few seconds for the container to actually spin up. Once it hit &lt;code&gt;Running&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="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; env-single-secret &lt;span class="nt"&gt;--&lt;/span&gt; /bin/sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'echo $SECRET_USERNAME'&lt;/span&gt;
adeoye-admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there it is — the secret successfully injected as an env var.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ConfigMaps&lt;/strong&gt; decouple non-sensitive configuration from your pod manifests, and can be reused across multiple pods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets&lt;/strong&gt; work the same way but are the right home for sensitive values.&lt;/li&gt;
&lt;li&gt;Both can be injected into a pod's containers via &lt;code&gt;env.valueFrom.configMapKeyRef&lt;/code&gt; or &lt;code&gt;secretKeyRef&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Watch your shell syntax — a stray space after a line-continuation &lt;code&gt;\&lt;/code&gt; will silently mangle your command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pods are mostly immutable&lt;/strong&gt; once created. Only image, tolerations, and a couple of grace-period fields can be patched live. Everything else means delete + recreate, or &lt;code&gt;kubectl replace --force&lt;/code&gt;. This is a big part of why Deployments exist.&lt;/li&gt;
&lt;li&gt;Give newly created pods a moment to leave &lt;code&gt;ContainerCreating&lt;/code&gt; before you &lt;code&gt;exec&lt;/code&gt; into them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's Day 19 done — small errors, but each one taught me something I'll remember longer than if it had just worked on the first try.&lt;/p&gt;




</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>40daysofkubernetes</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Day 17/40 — Kubernetes Autoscaling: HPA vs VPA Explained With Hands-On Practice</title>
      <dc:creator>Adeoye Malumi</dc:creator>
      <pubDate>Wed, 01 Jul 2026 12:09:21 +0000</pubDate>
      <link>https://dev.to/oyebobs/day-1740-kubernetes-autoscaling-hpa-vs-vpa-explained-with-hands-on-practice-2in1</link>
      <guid>https://dev.to/oyebobs/day-1740-kubernetes-autoscaling-hpa-vs-vpa-explained-with-hands-on-practice-2in1</guid>
      <description>&lt;p&gt;If you've ever wondered how Kubernetes knows when to spin up more pods or give a pod more memory, that's autoscaling — and it's one of those concepts that sounds intimidating until you actually do it yourself. Day 17 of the &lt;strong&gt;#40DaysOfKubernetes&lt;/strong&gt; challenge is where it clicked for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Autoscaling in Kubernetes?
&lt;/h2&gt;

&lt;p&gt;At its core, autoscaling means Kubernetes adjusts resources automatically based on demand. You don't manually intervene every time traffic spikes. There are two main types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HPA (Horizontal Pod Autoscaler)&lt;/strong&gt; — adds or removes &lt;em&gt;pods&lt;/em&gt; based on CPU/memory usage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPA (Vertical Pod Autoscaler)&lt;/strong&gt; — adjusts the &lt;em&gt;resources&lt;/em&gt; (CPU/memory) of existing pods&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of HPA as hiring more staff when the shop gets busy. VPA is more like giving one staff member more tools to handle the workload alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Did — Setting Up HPA
&lt;/h2&gt;

&lt;p&gt;First I deployed the sample &lt;code&gt;php-apache&lt;/code&gt; app with defined CPU requests and limits:&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;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;php-apache&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;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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php-apache&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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php-apache&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;php-apache&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/hpa-example&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;containerPort&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;resources&lt;/span&gt;&lt;span class="pi"&gt;:&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;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;500m&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="s"&gt;200m&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;php-apache&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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php-apache&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;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;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;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php-apache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffcryk4ykh0pg2e6g4vuw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffcryk4ykh0pg2e6g4vuw.png" alt="Applying the yaml file" width="593" height="54"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3n9jt6ugn5ifu1s3jak3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3n9jt6ugn5ifu1s3jak3.png" alt="The pod is up and running" width="669" height="79"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key part is setting &lt;code&gt;resources.requests.cpu&lt;/code&gt; — HPA needs this to calculate utilization. Without it, the autoscaler has nothing to measure against.&lt;/p&gt;

&lt;p&gt;Then I created the HPA object targeting &lt;strong&gt;50% average CPU utilization&lt;/strong&gt;, with a minimum of 1 pod and maximum of 10. This is the declarative method:&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;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;php-apache&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;php-apache&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;1&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;50&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this is the imperative method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl autoscale deployment php-apache &lt;span class="nt"&gt;--cpu-percent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50 &lt;span class="nt"&gt;--min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;--max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcp8kogxyhe1c8dt87oul.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcp8kogxyhe1c8dt87oul.png" alt="autoscale applied" width="800" height="41"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftmj616n2d70tpempd998.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftmj616n2d70tpempd998.png" alt="Autoscale complete" width="768" height="50"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating Load to Watch It Scale
&lt;/h2&gt;

&lt;p&gt;This is the fun part. I ran a load generator in a separate pod — basically a loop hammering the apache service with requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--tty&lt;/span&gt; load-generator &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox:1.28 &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /bin/sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"while sleep 0.01; do wget -q -O- http://php-apache; done"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then watched the HPA respond in real time:&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 hpa php-apache &lt;span class="nt"&gt;--watch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fubjwvgn5z5ggr2zrm573.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fubjwvgn5z5ggr2zrm573.png" alt=" " width="800" height="133"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Watching the replica count climb from 1 to several pods as CPU utilization crossed 50% made the whole concept land in a way that reading documentation never does.&lt;/p&gt;

&lt;h2&gt;
  
  
  HPA vs VPA — When Do You Use Which?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;HPA&lt;/th&gt;
&lt;th&gt;VPA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scales&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Number of pods&lt;/td&gt;
&lt;td&gt;Pod resource limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stateless apps with variable traffic&lt;/td&gt;
&lt;td&gt;Apps where sizing is hard to predict upfront&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Works with&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CPU, memory, custom metrics&lt;/td&gt;
&lt;td&gt;CPU and memory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In practice, most production workloads use HPA. VPA is useful during early deployment when you're still figuring out the right resource requests for an app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Don't skip setting &lt;code&gt;resources.requests&lt;/code&gt; in your deployment spec. HPA is blind without it. That one line is what connects your workload to the autoscaler.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>cka</category>
    </item>
    <item>
      <title>Resource Requests and Limits in Kubernetes (Hands-on with Metrics Server) #40DaysOfKubernetes – Task 16/40</title>
      <dc:creator>Adeoye Malumi</dc:creator>
      <pubDate>Fri, 19 Jun 2026 16:18:45 +0000</pubDate>
      <link>https://dev.to/oyebobs/resource-requests-and-limits-in-kubernetes-hands-on-with-metrics-server-40daysofkubernetes--580m</link>
      <guid>https://dev.to/oyebobs/resource-requests-and-limits-in-kubernetes-hands-on-with-metrics-server-40daysofkubernetes--580m</guid>
      <description>&lt;h2&gt;
  
  
  What this is about
&lt;/h2&gt;

&lt;p&gt;Today’s mission was simple on paper:&lt;br&gt;
Teach Kubernetes how to handle memory politely.&lt;/p&gt;

&lt;p&gt;In reality:&lt;br&gt;
Kubernetes behaved like a strict landlord, and my containers learned consequences.&lt;/p&gt;

&lt;p&gt;We explored:&lt;/p&gt;

&lt;p&gt;memory requests (what you ask for)&lt;br&gt;
memory limits (what you’re allowed to touch)&lt;br&gt;
what happens when containers ignore both&lt;/p&gt;
&lt;h4&gt;
  
  
  Step 1: Creating a safe playground (namespace)
&lt;/h4&gt;

&lt;p&gt;I started by isolating everything so I wouldn’t accidentally break the entire cluster (again):&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 namespace mem-example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fegk1vj96s6pyhcszn4mw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fegk1vj96s6pyhcszn4mw.png" alt=" " width="499" height="161"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Installing Metrics Server (the “fitness tracker” for pods)
&lt;/h4&gt;

&lt;p&gt;Metrics Server is basically Kubernetes’ way of checking:&lt;/p&gt;

&lt;p&gt;“Who is eating all the RAM?”&lt;/p&gt;

&lt;p&gt;Installed it using the provided YAML.&lt;/p&gt;

&lt;p&gt;Then… immediately learned Kubernetes loves drama.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem #1: Image pull failure 😭&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My pod refused to start:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Translation:&lt;/p&gt;

&lt;p&gt;“I tried to download your image… but the internet said no.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz1tl2xmdcopgjp7orm7x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz1tl2xmdcopgjp7orm7x.png" alt=" " width="800" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So before I could even stress memory, Kubernetes was already stressed.&lt;/p&gt;

&lt;p&gt;Then I sorted it out by deleting the metrics server and restarting it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhz82t88h1rkmzrgndfhw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhz82t88h1rkmzrgndfhw.png" alt=" " width="538" height="50"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: A well-behaved container (for once)
&lt;/h4&gt;

&lt;p&gt;Now I created a pod with rules:&lt;/p&gt;

&lt;p&gt;Request: 100Mi (be reasonable)&lt;br&gt;
Limit: 200Mi (don’t get greedy)&lt;br&gt;
resources:&lt;br&gt;
  requests:&lt;br&gt;
    memory: "100Mi"&lt;br&gt;
  limits:&lt;br&gt;
    memory: "200Mi"&lt;/p&gt;

&lt;p&gt;And then I told it:&lt;/p&gt;

&lt;p&gt;“You may use 150Mi, but don’t embarrass me.”&lt;/p&gt;

&lt;p&gt;Command:&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; memory-request-limit.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F41dd4vqh8epb3q1gn91c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F41dd4vqh8epb3q1gn91c.png" alt=" " width="681" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What happened?&lt;/p&gt;

&lt;p&gt;Shockingly… it behaved.&lt;/p&gt;

&lt;p&gt;Checked usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl top pod memory-demo &lt;span class="nt"&gt;-n&lt;/span&gt; mem-example

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

&lt;/div&gt;



&lt;p&gt;It used ~150Mi.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fliaptek4yh9y9t90acx6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fliaptek4yh9y9t90acx6.png" alt=" " width="635" height="56"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So Kubernetes basically said:&lt;/p&gt;

&lt;p&gt;“You asked for 100Mi, but I’ll allow vibes up to 200Mi. Based off the yaml specifications”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fizvv6ji1wsoj80ysdc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fizvv6ji1wsoj80ysdc.png" alt=" " width="759" height="374"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Step 4: The “don’t exceed your limit” experiment 💀
&lt;/h4&gt;

&lt;p&gt;Now I got ambitious (or careless).&lt;/p&gt;

&lt;p&gt;Created another pod:&lt;/p&gt;

&lt;p&gt;Request: 50Mi&lt;br&gt;
Limit: 100Mi&lt;br&gt;
Actual attempt: 250Mi (yes… I pushed it)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3gxexbuylzcf5i59byof.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3gxexbuylzcf5i59byof.png" alt=" " width="680" height="349"&gt;&lt;/a&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 apply &lt;span class="nt"&gt;-f&lt;/span&gt; mem2.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result: Kubernetes said NO.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OOMKilled
Exit code 137
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frt9afle4vhruqcjyydgc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frt9afle4vhruqcjyydgc.png" alt=" " width="450" height="112"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The container basically got:&lt;/p&gt;

&lt;p&gt;“You exceeded your budget. Session terminated.”&lt;/p&gt;

&lt;p&gt;And then it restarted… and died again… and again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs3xql4057xwkv4awvo0e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs3xql4057xwkv4awvo0e.png" alt=" " width="799" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ve9qfr2jbzzcem9r6xo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ve9qfr2jbzzcem9r6xo.png" alt=" " width="800" height="129"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So yes, I successfully created a loop of failure.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 5: The “impossible request” experiment
&lt;/h4&gt;

&lt;p&gt;Now I tried something unrealistic:&lt;/p&gt;

&lt;p&gt;Request: 1000Gi memory&lt;br&gt;
Limit: 1000Gi&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjvvbj873zv7gov5wk6ty.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjvvbj873zv7gov5wk6ty.png" alt=" " width="750" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Basically:&lt;/p&gt;

&lt;p&gt;“I would like half the cluster please.”&lt;/p&gt;

&lt;p&gt;Result:&lt;/p&gt;

&lt;p&gt;Pod stayed in Pending&lt;br&gt;
No node could even consider it&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F796coiastgj95m277503.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F796coiastgj95m277503.png" alt=" " width="800" height="204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvfcmzrb9oovbmb6hsj7h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvfcmzrb9oovbmb6hsj7h.png" alt=" " width="799" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flqif89kgtfoqzvdu60y7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flqif89kgtfoqzvdu60y7.png" alt=" " width="800" height="46"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes response:&lt;/p&gt;

&lt;p&gt;“Be serious.”&lt;/p&gt;

&lt;p&gt;What I learned (the real value)&lt;br&gt;
Requests are not suggestions — they decide where your pod even goes&lt;br&gt;
Limits are not suggestions — they decide how your pod dies&lt;br&gt;
Kubernetes is very polite… until you exceed limits&lt;br&gt;
OOMKilled is just Kubernetes saying “enough”&lt;br&gt;
If your request is unrealistic, your pod doesn’t crash — it just never gets invited in&lt;/p&gt;

&lt;p&gt;The Tutorial I followed:&lt;br&gt;
  &lt;iframe src="https://www.youtube.com/embed/Q-mk6EZVX_Q"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>learninginpublic</category>
      <category>devopsengineer</category>
      <category>cloudengineer</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Predicting Tomorrow's Tremors: A Machine Learning Approach to Earthquake Nowcasting in California</title>
      <dc:creator>Adeoye Malumi</dc:creator>
      <pubDate>Thu, 03 Jul 2025 12:56:11 +0000</pubDate>
      <link>https://dev.to/oyebobs/predicting-tomorrows-tremors-a-machine-learning-approach-to-earthquake-nowcasting-in-california-l0i</link>
      <guid>https://dev.to/oyebobs/predicting-tomorrows-tremors-a-machine-learning-approach-to-earthquake-nowcasting-in-california-l0i</guid>
      <description>&lt;p&gt;Earthquakes are a constant, terrifying reality, especially in tectonically active zones like California. While pinpointing the exact time and location of a future quake remains one of science's grand challenges, the concept of earthquake nowcasting offers a pragmatic alternative: assessing the current probability of a significant event happening within a near-term window.&lt;/p&gt;

&lt;p&gt;This article walks through the entire journey of building and deploying a machine learning model designed to nowcast the likelihood of Magnitude 6.0+ earthquakes in California within a 30-day horizon. I'll cover everything from robust data acquisition to feature engineering, model training, and the practicalities of deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;1. The Bedrock: Data Acquisition&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Every data-driven project starts with data. For me, this meant building a comprehensive historical catalog of seismic events in California.&lt;/p&gt;

&lt;p&gt;I leveraged the ObsPy library to interact with the USGS FDSN client. My goal was to gather all earthquakes with a Magnitude 2.0 or greater (M2+) within a specific region (32.0°N to 42.0°N latitude, -125.0°W to -114.0°W longitude) from 1990 to the present day.&lt;/p&gt;

&lt;p&gt;One of the initial hurdles was dealing with potential API request limits when trying to fetch decades of data at once. To overcome this, I implemented a robust chunking mechanism. Instead of one massive request, I'd iteratively fetch data in smaller time windows (starting with years, then recursively breaking down into months, weeks, or even days if a chunk proved too large). This ensured I could reliably acquire the entire historical catalog without hitting service caps. The collected data was then saved locally as a CSV for efficient reuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;2. Sculpting Signals: Feature Engineering&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Raw earthquake event lists aren't directly useful for machine learning. The magic happens in feature engineering – transforming this raw data into meaningful numerical representations that the model can learn from.&lt;/p&gt;

&lt;p&gt;For each sliding time window, I calculated a rich set of features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Regional Features (Across the Entire California Study Area)&lt;/strong&gt;:&lt;br&gt;
These capture the overall seismic state of the broader region:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Seismicity Rate&lt;/strong&gt;: The total number of events within the window.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;b-value&lt;/strong&gt;: A critical seismological parameter indicating the ratio of small to large earthquakes. A decrease in b-value can sometimes precede larger events, suggesting increased stress.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Magnitude Statistics&lt;/strong&gt;: Mean, standard deviation, and maximum magnitude.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Inter-Event Time Statistics&lt;/strong&gt;: Mean and coefficient of variation of the time between successive earthquakes. Irregularity (high CV) might be a signal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Depth Statistics&lt;/strong&gt;: Mean and standard deviation of earthquake depths.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Spatial Features (Per Grid Cell):
&lt;/h3&gt;

&lt;p&gt;To capture localized patterns, I divided the entire California region into a grid of 0.5-degree by 0.5-degree cells. For each cell, if it contained at least 3 events within the window (our MIN_EVENTS_PER_CELL threshold to ensure statistical significance), I calculated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Local Seismicity Rate&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Local b-value&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Local Mean Magnitude&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used a 90-day sliding window to compute these features, advancing the window by 7 days for each new sample. The target label for the model was binary: 1 if a Magnitude 6.0+ earthquake occurred within the subsequent 30-day prediction horizon after the feature window, and 0 otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Brain: Model Training &amp;amp; Evaluation
&lt;/h2&gt;

&lt;p&gt;With our features ready, it was time to train the predictive brain of our system.&lt;/p&gt;

&lt;p&gt;I chose XGBoost Classifier as the core model. It's a powerful gradient boosting framework known for its performance and ability to handle complex, tabular datasets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tackling Class Imbalance
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Earthquake nowcasting suffers from extreme class imbalance&lt;/strong&gt;: periods without a large earthquake significantly outnumber periods preceding one. To address this, we employed several strategies:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stratified Splitting&lt;/strong&gt;: When splitting data into training and test sets, we used stratify=y to ensure both sets maintained the original proportion of large earthquake events.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SMOTE (Synthetic Minority Over-sampling Technique)&lt;/strong&gt;: Applied to the training data, SMOTE generated synthetic samples of the minority class (large earthquake events), effectively balancing the dataset for the model to learn from. We dynamically adjusted SMOTE's k_neighbors parameter to ensure it always had enough real minority samples to work with.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Hyperparameter Tuning &amp;amp; Evaluation
&lt;/h3&gt;

&lt;p&gt;We performed hyperparameter tuning using GridSearchCV, focusing on optimizing the model's F1-score. The F1-score is particularly valuable for imbalanced datasets as it provides a balance between precision (minimizing false positives) and recall (minimizing false negatives).&lt;/p&gt;

&lt;p&gt;After training, the model's performance was rigorously evaluated on the untouched test set. I examined:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Classification Report&lt;/strong&gt;: Providing precision, recall, and F1-score for both classes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ROC-AUC Score&lt;/strong&gt;: A measure of the model's ability to distinguish between classes across all possible thresholds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Confusion Matrix&lt;/strong&gt;: A visual breakdown of true positives, true negatives, false positives, and false negatives.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's an example of a Confusion Matrix from a training run:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcwqqhk98e14awjhdoz9e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcwqqhk98e14awjhdoz9e.png" alt="Confusion Matrix from a training run" width="600" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We also analyzed the Precision-Recall Curve &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq2q1ey20ig71wyeei95c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq2q1ey20ig71wyeei95c.png" alt="Precision-Recall Curve " width="700" height="600"&gt;&lt;/a&gt;, which is often more informative than ROC for imbalanced datasets:&lt;/p&gt;

&lt;p&gt;Finally, we looked at &lt;strong&gt;Feature Importance&lt;/strong&gt; to understand which seismic indicators the XGBoost model deemed most influential in its predictions. Features related to regional seismicity rate, standard deviation of depth, coefficient of variation of inter-event time, and localized spatial b-values often topped the list.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Optimal Threshold
&lt;/h3&gt;

&lt;p&gt;Crucially, we didn't just rely on the model's default 0.5 probability threshold. We analyzed the Precision, Recall, and F1-score across a range of thresholds and identified the optimal F1-score threshold as 0.3593. This value provides the best balance between catching large earthquakes and avoiding excessive false alarms for our specific model.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4eai4i16hpg712jz6soh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4eai4i16hpg712jz6soh.png" alt="Precision, Recall, and F1-score" width="800" height="560"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Bringing it Live: Model Deployment
&lt;/h2&gt;

&lt;p&gt;Training a great model is one thing; making it useful in a real-world setting is another. This required careful deployment steps.&lt;/p&gt;
&lt;h3&gt;
  
  
  Model Serialization
&lt;/h3&gt;

&lt;p&gt;After training, the best_model (our optimized XGBoost classifier) was saved to disk using joblib. But there's a critical detail: XGBoost models are sensitive to the order of input features. So, alongside the model, we also saved the exact ordered list of feature column names that the model was trained on. This ensures that when the model is loaded later for prediction, the incoming data is always presented in the correct sequence.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Python

import joblib
# ... after model training ...
joblib.dump(best_model, "earthquake_prediction_model.joblib")
joblib.dump(X.columns.tolist(), "model_feature_columns.joblib")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Prediction Script (predict_earthquake.py)
&lt;/h3&gt;

&lt;p&gt;A separate, lightweight Python script (predict_earthquake.py) was created specifically for making live predictions. This script is designed to run independently, without needing to retrain the model. Its core functions are:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load Assets&lt;/strong&gt;: It loads the saved earthquake_prediction_model.joblib and the model_feature_columns.joblib list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fetch Latest Data&lt;/strong&gt;: It connects to the USGS FDSN client to fetch only the most recent earthquake data required for the current 90-day feature window (ending at the current time).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistent Feature Engineering&lt;/strong&gt;: It applies the exact same feature engineering logic as the training pipeline to this latest data. This consistency is paramount. It also handles cases where certain grid cells might be inactive in the current window by filling their features with zeros, matching how the training data was prepared.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Predict&lt;/strong&gt;: The engineered features are passed to the loaded model, which outputs a probability score.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Threshold &amp;amp; Alert&lt;/strong&gt;: The pre-determined optimal threshold of 0.3593 is applied. If the predicted probability exceeds this, an alert is triggered.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Staying Vigilant: Automation &amp;amp; Alerting
&lt;/h2&gt;

&lt;p&gt;A prediction system is only useful if it runs consistently and communicates its findings effectively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automated Scheduling&lt;/strong&gt;&lt;br&gt;
To ensure continuous nowcasting, predict_earthquake.py was automated to run at regular intervals (e.g., daily). This was set up using:&lt;/p&gt;

&lt;p&gt;cron jobs for Linux/macOS environments, which allow scheduling commands to run at specific times.&lt;/p&gt;

&lt;p&gt;Task Scheduler for Windows, providing a graphical interface for similar functionality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Robust Alerting &amp;amp; Logging&lt;/strong&gt;&lt;br&gt;
Beyond simple console output, the system was enhanced for practical deployment:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dedicated Log File&lt;/strong&gt;: All prediction cycle information – data fetches, warnings (like NaN values being filled), and final predictions – are written to a dedicated log file (earthquake_prediction.log). This is invaluable for monitoring the system's health and troubleshooting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email Notifications&lt;/strong&gt;: Crucially, if a "Large Quake" prediction is made (probability &amp;gt;= 0.3593), the script is configured to send an immediate email alert. This ensures that relevant stakeholders are notified without needing to constantly monitor logs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Python

# Conceptual snippet for email alert in predict_earthquake.py
import smtplib
from email.mime.text import MIMEText
import logging # Already configured earlier

# ... email config variables ...

def send_email_alert(subject, body):
    try:
        msg = MIMEText(body)
        msg["Subject"] = subject
        msg["From"] = EMAIL_SENDER
        msg["To"] = EMAIL_RECEIVER
        with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
            server.starttls()
            server.login(EMAIL_SENDER, EMAIL_PASSWORD)
            server.send_message(msg)
        logging.info(f"Email alert sent successfully to {EMAIL_RECEIVER}")
    except Exception as e:
        logging.error(f"Failed to send email alert: {e}")

# ... later in make_prediction() ...
if prediction_label == 1:
    alert_message = (f"ALERT! A large earthquake (M{TARGET_MAGNITUDE}+) is predicted "
                     f"in California within the next {PREDICTION_HORIZON_DAYS} days.\n"
                     f"Probability: {prediction_proba:.4f}")
    logging.warning(f"Prediction: {alert_message}")
    send_email_alert("Earthquake Prediction ALERT!", alert_message)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This project successfully establishes a complete, automated pipeline for earthquake nowcasting in California using machine learning. From meticulously gathering and engineering seismic data to training a robust XGBoost model and deploying it with automated scheduling and alerting, the system represents a significant step towards leveraging data science for natural hazard preparedness.&lt;/p&gt;

&lt;p&gt;While the inherent complexities of earthquake prediction mean no model is perfect, this system provides a valuable, data-driven assessment of current seismic risk. The journey highlights the importance of not just model accuracy, but also the practical considerations of data handling, feature consistency, and operational deployment in building real-world ML solutions.&lt;/p&gt;

&lt;p&gt;The next steps involve continuous monitoring of the system's performance, periodic retraining with updated data to ensure relevance, and potentially exploring more advanced validation techniques like time-series cross-validation for even greater robustness.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Full Project Code (GitHub)&lt;/strong&gt;:&lt;a href="https://github.com/oye-bobs/siesmicanalyzer" rel="noopener noreferrer"&gt;(https://github.com/oye-bobs/siesmicanalyzer)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ObsPy Library&lt;/strong&gt;: &lt;a href="https://docs.obspy.org/" rel="noopener noreferrer"&gt;https://docs.obspy.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;USGS FDSN Web Service&lt;/strong&gt;: &lt;a href="https://earthquake.usgs.gov/ws/" rel="noopener noreferrer"&gt;https://earthquake.usgs.gov/ws/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scikit-learn Documentation&lt;/strong&gt;: &lt;a href="https://scikit-learn.org/" rel="noopener noreferrer"&gt;https://scikit-learn.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XGBoost Documentation&lt;/strong&gt;: &lt;a href="https://xgboost.readthedocs.io/" rel="noopener noreferrer"&gt;https://xgboost.readthedocs.io/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>python</category>
      <category>datascience</category>
      <category>coding</category>
    </item>
    <item>
      <title>Embracing Open Source: A Catalyst for Scientific Progress</title>
      <dc:creator>Adeoye Malumi</dc:creator>
      <pubDate>Fri, 19 Apr 2024 13:41:52 +0000</pubDate>
      <link>https://dev.to/oyebobs/embracing-open-source-a-catalyst-for-scientific-progress-1n0</link>
      <guid>https://dev.to/oyebobs/embracing-open-source-a-catalyst-for-scientific-progress-1n0</guid>
      <description>&lt;p&gt;Hey Dev Community!&lt;/p&gt;

&lt;p&gt;Let's embark on a thrilling journey through the realm of open source, where innovation knows no bounds and collaboration reigns supreme! &lt;/p&gt;

&lt;p&gt;Today, we're diving into why open source is the ultimate game-changer for scientific progress and how it's shaping a brighter future for us all. Get ready for a wild ride!&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Open Source Matters in Science:
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Transparency&lt;/strong&gt;: Open source is like a spotlight shining on the secrets of science. It unveils research methodologies, algorithms, and data for all to see, fostering a culture of transparency and trust within the scientific community.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Collaboration&lt;/strong&gt;: Imagine a global science party where everyone's invited! That's the power of open source. By tearing down barriers to access, it brings together brilliant minds from around the world to collaborate on groundbreaking research.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Faster Iteration&lt;/strong&gt;: With open source, the innovation treadmill goes into overdrive! Scientists can build upon existing solutions, tweak them to perfection, and share their creations with the world—all at warp speed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reproducibility&lt;/strong&gt;: Science is all about trust, and open source ensures that trust never falters. By providing open access to source code and data, it makes research findings easily reproducible, empowering others to verify and validate discoveries.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Illustrating the Impact:
&lt;/h2&gt;

&lt;p&gt;Let's take a joyride through the world of climate science, where open source is leading the charge:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data Sharing&lt;/strong&gt;: Climate data from every corner of the globe flows freely, thanks to open source platforms like Open Climate GIS. This treasure trove of data fuels cutting-edge research and fuels our understanding of Earth's ever-changing climate.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Collaborative Research&lt;/strong&gt;: Scientists from diverse fields come together on platforms like Open Climate Collaborative to tackle climate change head-on. From atmospheric physicists to data wizards, everyone brings their A-game to the fight against the climate crisis.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reproducibility&lt;/strong&gt;: The winds of change blow strong in the world of climate research, where transparency reigns supreme. Researchers openly share their methodologies, code, and data, ensuring that every discovery is as solid as a rock.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Innovation&lt;/strong&gt;: Open-source innovation is the wind beneath our wings as we soar towards a greener future. From AI-powered climate models to blockchain-based carbon markets, the possibilities are endless!&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Conclusion&lt;/em&gt;&lt;/strong&gt;:&lt;br&gt;
So there you have it, fellow adventurers—open source isn't just a buzzword; it's the key to unlocking a world of endless possibilities. In the grand quest for scientific progress, let's hoist the open source flag high and march boldly into the future!&lt;/p&gt;

&lt;p&gt;What are your thoughts on the role of open source in scientific progress? Share your insights below and let's keep the conversation going! &lt;/p&gt;

</description>
      <category>opensource</category>
      <category>datascience</category>
      <category>database</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>What was your win this week?</title>
      <dc:creator>Adeoye Malumi</dc:creator>
      <pubDate>Fri, 19 Apr 2024 13:28:17 +0000</pubDate>
      <link>https://dev.to/oyebobs/what-was-your-win-this-week-51f</link>
      <guid>https://dev.to/oyebobs/what-was-your-win-this-week-51f</guid>
      <description>&lt;p&gt;I started coding again and I've been able to learn a whole lot on github. from learning how to use code spaces to developing my own portfolio page, It's been a really interesting journey.&lt;/p&gt;

</description>
      <category>weeklyretro</category>
    </item>
  </channel>
</rss>
