<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Kubernetes]]></title><description><![CDATA[Kubernetes]]></description><link>https://kubernetes-sagnik.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 01 Sep 2026 20:44:42 GMT</lastBuildDate><atom:link href="https://kubernetes-sagnik.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Kubernetes Fundamentals, Part 5: Managing Complexity with Helm]]></title><description><![CDATA[In the first four parts of this series, we journeyed through the core of Kubernetes. We built clusters, deployed Pods, connected them with Services, and learned exactly how the Scheduler decides where]]></description><link>https://kubernetes-sagnik.hashnode.dev/kubernetes-fundamentals-part-5-managing-complexity-with-helm</link><guid isPermaLink="true">https://kubernetes-sagnik.hashnode.dev/kubernetes-fundamentals-part-5-managing-complexity-with-helm</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Sagnik Guru]]></dc:creator><pubDate>Thu, 26 Mar 2026 23:43:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6802f5e18e6fd72ba8c17773/bc2b141f-c8b6-4904-87b0-068a98f6fc18.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the first four parts of this series, we journeyed through the core of Kubernetes. We built clusters, deployed Pods, connected them with Services, and learned exactly how the Scheduler decides where they should live.</p>
<p>If you have been following along and building your own applications, you have probably noticed a growing problem: <strong>YAML fatigue</strong>.</p>
<p>To deploy a simple full-stack application, you need a Deployment, a Service, an Ingress, maybe a ConfigMap, a Secret, and a HorizontalPodAutoscaler. That is easily 300+ lines of raw YAML. Worse, if you want to deploy that exact same application to three different environments (Dev, Staging, and Production), you either have to copy-paste those 300 lines three times, or write complex bash scripts to inject different environment variables.</p>
<p>There has to be a better way. Enter <strong>Helm</strong>, the package manager for Kubernetes. Today, in the final part of our series, we are going to learn how to tame the YAML beast, package our applications logically, and deploy them like professionals.</p>
<h2>1. What is Helm?</h2>
<p>If you use a Mac, you probably use <code>Homebrew</code> (<code>brew install node</code>). If you use Ubuntu, you use <code>APT</code> (<code>apt install nginx</code>). Helm is the exact same concept, but for Kubernetes.</p>
<p>Helm allows you to bundle all of your scattered Kubernetes YAML files into a single, logical, version-controlled package called a <strong>Helm Chart</strong>.</p>
<p>Instead of running:</p>
<pre><code class="language-shell">kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f ingress.yaml
</code></pre>
<p>You simply run:</p>
<pre><code class="language-shell">helm install my-app ./my-app-chart
</code></pre>
<p>Helm takes your chart, renders the final YAML, and sends it to the Kubernetes API server all at once.</p>
<h2>2. The Core Concept: Templating</h2>
<p>The real power of Helm is not just grouping files together; it is <strong>templating</strong>. Helm uses the Go templating engine to inject dynamic variables into your otherwise static YAML manifests.</p>
<p>When you generate a new Helm chart (using <code>helm create my-app</code>), Helm builds a specific directory structure:</p>
<pre><code class="language-shell">my-app/
  Chart.yaml          # Metadata about the chart (name, version)
  values.yaml         # The default variables for your templates
  templates/          # The actual YAML files containing Go templates
</code></pre>
<h3>How Templating Works</h3>
<p>Let's look at a raw Kubernetes Service YAML:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: frontend-service
spec:
  type: ClusterIP
  ports:
    - port: 80
</code></pre>
<p>If we want to make this dynamic, we move it into the <code>templates/</code> folder and replace the hardcoded values with Helm variables (which are wrapped in <code>{{ }}</code>):</p>
<pre><code class="language-yaml"># templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}-service
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
</code></pre>
<p>Where do those values come from? They come from the <code>values.yaml</code> file at the root of your chart:</p>
<pre><code class="language-yaml"># values.yaml
service:
  type: ClusterIP
  port: 80
</code></pre>
<p>When you run <code>helm install</code>, Helm merges the <code>values.yaml</code> with the templates, compiles the final valid Kubernetes YAML, and deploys it.</p>
<h2>3. The Multi-Environment Superpower</h2>
<p>Why is templating so valuable? Because it completely solves the multi-environment problem.</p>
<p>You keep <strong>one</strong> set of templates, but you create different values files for different environments.</p>
<p>For example, you might create a <code>values-prod.yaml</code>:</p>
<pre><code class="language-yaml"># values-prod.yaml
replicaCount: 10
service:
  type: LoadBalancer
image:
  tag: "v2.0.0"
</code></pre>
<p>And a <code>values-dev.yaml</code>:</p>
<pre><code class="language-yaml"># values-dev.yaml
replicaCount: 1
service:
  type: NodePort
image:
  tag: "latest-dev"
</code></pre>
<p>To deploy to production, you just pass the specific values file to the Helm command:</p>
<pre><code class="language-shell">helm install my-app ./my-app-chart -f values-prod.yaml
</code></pre>
<p>You have successfully decoupled your application's <em>structure</em> (the templates) from its <em>environment data</em> (the values).</p>
<h2>4. Releases and Rollbacks</h2>
<p>If you deploy raw YAML with <code>kubectl apply</code>, Kubernetes just blindly updates the state. If you make a mistake and break production, how do you undo it? You have to manually find the previous YAML file and re-apply it.</p>
<p>Helm solves this with <strong>Releases</strong>. Every time you install or upgrade a chart, Helm creates a tracked Release in the cluster. It remembers exactly what YAML it generated and applied.</p>
<p>If you deploy version 2 of your app and it crashes, rolling back is literally one command:</p>
<pre><code class="language-shell"># See your deployment history
helm history my-app

# Rollback to revision 1
helm rollback my-app 1
</code></pre>
<p>Helm instantly reverts every single Kubernetes resource (Deployments, Services, ConfigMaps) back to exactly how they were in revision 1.</p>
<h2><strong>5. The Brain of the Package:</strong> <code>Chart.yaml</code></h2>
<p>If <code>templates/</code> holds your structure and <code>values.yaml</code> holds your data, what does <code>Chart.yaml</code> do?</p>
<p>The <code>Chart.yaml</code> file is the <strong>identity and metadata</strong> of your Helm chart. It tells Helm exactly what it is packaging. When you type <code>helm list</code> to see what is running in your cluster, the information you see comes directly from this file.</p>
<p>A typical <code>Chart.yaml</code> looks like this:</p>
<pre><code class="language-yaml">apiVersion: v2 
name: my-frontend-app 
description: A React frontend for our e-commerce platform type: application version: 1.1.0 # The version of this Helm Chart 
appVersion: "2.4.5" # The version of the underlying application code 
dependencies:         # External charts required by your app
  - name: redis
    version: "17.3.14"
    repository: "https://charts.bitnami.com/bitnami"
</code></pre>
<p><strong>Why is this file so important?</strong></p>
<ol>
<li><p><strong>Versioning:</strong> Notice there are <em>two</em> versions. <code>appVersion</code> is the version of your actual code (e.g., your Docker image tag like <code>v2.4.5</code>). But <code>version</code> is the version of the <em>Helm Chart itself</em> (e.g., <code>1.1.0</code>). If you update your Kubernetes Service from a <code>ClusterIP</code> to a <code>NodePort</code>, your application code hasn't changed, but your infrastructure has. You would bump the Chart <code>version</code> to <code>1.1.1</code> to track that infrastructure change.</p>
</li>
<li><p><strong>Dependencies:</strong> If your frontend application cannot run without a Redis cache, you can actually declare Redis as a dependency inside your <code>Chart.yaml</code>. When you install your frontend chart, Helm will automatically go download and install the Redis chart first.</p>
</li>
<li><p><strong>Template Injection:</strong> Just like you can inject variables from <code>values.yaml</code>, you can also inject metadata from <code>Chart.yaml</code> directly into your templates using <code>{{ .</code><a href="http://Chart.Name"><code>Chart.Name</code></a> <code>}}</code> or <code>{{ .Chart.Version }}</code>.</p>
</li>
</ol>
<h2>6. Helm Chart Best Practices</h2>
<p>As you start writing your own charts, the templates can get messy quickly. Here are three industry-standard best practices to keep your charts clean:</p>
<p><strong>1. Use</strong> <code>_helpers.tpl</code> <strong>for Reusable Logic</strong> Never hardcode labels across multiple files. Helm allows you to define reusable snippets of code in a file called <code>_helpers.tpl</code>. You can define a standard set of labels once, and inject them into your Deployment, Service, and Ingress simultaneously.</p>
<p><strong>2. Follow Semantic Versioning strictly</strong> Your <code>Chart.yaml</code> has two version fields: <code>version</code> (the version of the Helm chart itself) and <code>appVersion</code> (the version of the Docker image it deploys). Always increment the <code>version</code> using Semantic Versioning (e.g., <code>1.0.0</code> to <code>1.0.1</code>) whenever you change the templates.</p>
<p><strong>3. Never put passwords in</strong> <code>values.yaml</code> Values files are meant to be checked into Git. Do not put database passwords or API keys in your <code>values.yaml</code>. Instead, inject them at runtime using CI/CD pipelines:</p>
<pre><code class="language-shell">helm upgrade my-app ./my-app-chart --set database.password=$SECRET_VAR
</code></pre>
<hr />
<p>And with that, we have reached the end of our <strong>Kubernetes Fundamentals</strong> series!</p>
<p>Thank you for following along on this journey. Reach out to me on <a href="https://x.com/SagnikGuru">Twitter</a> and/or <a href="https://www.linkedin.com/in/sagnik-guru-46790919a">LinkedIn</a></p>
<hr />
]]></content:encoded></item><item><title><![CDATA[Kubernetes Fundamentals, Part 4: The Scheduler, Node Selection, and Pod Placement]]></title><description><![CDATA[In the first three parts of this series, we learned how the Kubernetes architecture works, how to define workloads and storage, and how to route network traffic to our applications.
But there is a mas]]></description><link>https://kubernetes-sagnik.hashnode.dev/kubernetes-fundamentals-part-4-the-scheduler-node-selection-and-pod-placement</link><guid isPermaLink="true">https://kubernetes-sagnik.hashnode.dev/kubernetes-fundamentals-part-4-the-scheduler-node-selection-and-pod-placement</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Sagnik Guru]]></dc:creator><pubDate>Wed, 25 Mar 2026 00:28:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6802f5e18e6fd72ba8c17773/7ba72d73-f326-46ee-9fd2-c1a8aabbd88f.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the first three parts of this series, we learned how the Kubernetes architecture works, how to define workloads and storage, and how to route network traffic to our applications.</p>
<p>But there is a massive piece of the puzzle we have ignored until now. When you run <code>kubectl apply -f pod.yaml</code>, how does Kubernetes actually decide <em>which</em> specific server (Worker Node) your application should run on?</p>
<p>In a traditional Virtual Machine world, a sysadmin decides where an application goes. In Kubernetes, this is handled automatically by the <strong>kube-scheduler</strong>. Today, we are going to dive deep into the brain of Kubernetes. We will explore how the scheduler thinks, how to control where your Pods live using Taints and Affinity, how to bypass the scheduler entirely, and how to automatically scale your infrastructure when traffic spikes.</p>
<h2>1. The Core Mechanism: Labels and Selectors</h2>
<p>Before we can tell Kubernetes where to place things, we need to understand how Kubernetes identifies things. The entire Kubernetes ecosystem relies on a simple tagging system: <strong>Labels and Selectors</strong>.</p>
<ul>
<li><p><strong>Labels</strong> are key-value pairs attached to objects (like Pods or Nodes).</p>
</li>
<li><p><strong>Selectors</strong> are how other Kubernetes components find those objects.</p>
</li>
</ul>
<p>Here is what that looks like in YAML. We label a Pod, and a Service selects it:</p>
<pre><code class="language-yaml"># 1. We label the Pod
apiVersion: v1
kind: Pod
metadata:
  name: my-app
  labels:
    env: production
    tier: frontend
spec:
  containers:
    - name: nginx
      image: nginx
***
# 2. A Service selects the Pod using those labels
apiVersion: v1
kind: Service
metadata:
  name: frontend-service
spec:
  selector:
    env: production
    tier: frontend
</code></pre>
<h3>Labels and Node Selection</h3>
<p>Labels and selectors are also part of scheduling. Nodes can have labels just like Pods, for example <code>disktype=ssd</code> or <code>hardware=gpu</code>.</p>
<p>Kubernetes can use these node labels to decide where a Pod should run. The simplest way is <code>nodeSelector</code>, where a Pod explicitly states, "schedule me only on nodes with this label."</p>
<pre><code class="language-yaml"># First, label a node via CLI:
# kubectl label nodes worker-1 disktype=ssd

apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  nodeSelector:
    disktype: ssd
  containers:
    - name: nginx
      image: nginx
</code></pre>
<p>As we will see later, Node Affinity builds heavily on this exact same concept.</p>
<h2>2. The Mental Map: What happens when you create a Pod?</h2>
<p>To understand scheduling, you need to understand the exact timeline of a Pod's creation.</p>
<ol>
<li><p><strong>The Request:</strong> You run <code>kubectl apply -f pod.yaml</code>. The API Server validates the YAML and saves the Pod object into the <code>etcd</code> database.</p>
</li>
<li><p><strong>The Pending State:</strong> At this exact moment, the Pod's configuration contains a field called <code>nodeName</code> which is set to <code>null</code>. Because it has no node, the Pod is marked as <code>Pending</code>.</p>
</li>
<li><p><strong>The Scheduler Awakens:</strong> The <code>kube-scheduler</code>, which constantly watches the API Server for Pods with no <code>nodeName</code>, spots your Pod and begins its selection process.</p>
</li>
<li><p><strong>The Assignment:</strong> The Scheduler picks the best Node and updates the Pod's <code>nodeName</code> field in <code>etcd</code>. <strong>The Scheduler's job is now done.</strong> It does not start the container.</p>
</li>
<li><p><strong>The Execution:</strong> The <code>kubelet</code> (the agent running on the winning Node) sees that a Pod has been assigned to its machine. The <code>kubelet</code> downloads the container image and starts the application.</p>
</li>
<li><p><strong>The Runtime:</strong> Health probes and autoscalers take over monitoring the running application.</p>
</li>
</ol>
<p>The golden rule here is: <strong>The Scheduler only decides WHERE a Pod goes, not HOW it starts.</strong></p>
<h2>3. Inside the Scheduler: The Two-Phase Process</h2>
<p>When the Scheduler spots your <code>Pending</code> Pod, it looks at all the available worker nodes in your cluster and runs a two-phase elimination tournament.</p>
<h3>Phase 1: Filtering (The Elimination Round)</h3>
<p>In this phase, the Scheduler removes any node that physically or logically cannot run the Pod. The most critical filter is <strong>Resource Requests</strong>.</p>
<pre><code class="language-yaml">spec:
  containers:
  - name: my-app
    image: my-app:v1
    resources:
      requests:
        cpu: "500m"      # Half a CPU core
        memory: "512Mi"
      limits:
        cpu: "1000m"     # One full CPU core
        memory: "1Gi"
</code></pre>
<p><strong>A Major Gotcha:</strong> The Scheduler <em>only</em> looks at <code>requests</code>. Furthermore, it does not look at the actual, physical CPU currently being used on a Node. It looks at the <strong>Allocatable Capacity</strong> minus the <strong>Sum of Requests</strong> of all currently scheduled Pods.</p>
<p>If a Node has 4 CPUs, and is running 3 Pods that each <em>request</em> 1 CPU, the Scheduler sees 1 CPU available. Even if those 3 Pods are currently idle and using 0% physical CPU, the Scheduler will absolutely reject a new Pod that requests 2 CPUs. The Node is mathematically eliminated.</p>
<p><strong>In short,</strong> <code>requests</code> <strong>decide placement, while</strong> <code>limits</code> <strong>decide runtime boundaries.</strong></p>
<h3>Phase 2: Scoring (The Ranking Round)</h3>
<p>Once the Scheduler has a list of nodes that survived the filtering phase, it scores them. It assigns points based on built-in rules, such as prioritizing nodes that have the most free resources (to balance the cluster) or prioritizing nodes that already have the required container images downloaded. The Node with the highest score wins the Pod.</p>
<h2>4. Deep Dive: Node Affinity (Attracting Pods)</h2>
<p>Node Affinity works on top of <strong>node labels</strong>. In that sense, it is closely related to <code>nodeSelector</code>.</p>
<p>The difference is:</p>
<ul>
<li><p><code>nodeSelector</code> is the simple form: it requires an exact label match only.</p>
</li>
<li><p><code>nodeAffinity</code> is the advanced form: it still uses node labels, but supports operators like <code>In</code>, <code>NotIn</code>, and <code>Exists</code>, and also supports both hard and soft rules.</p>
</li>
</ul>
<p>There are two main types of Node Affinity, mapping perfectly to our two scheduling phases:</p>
<ol>
<li><code>requiredDuringSchedulingIgnoredDuringExecution</code> <strong>(The Hard Rule)</strong> Evaluated during the <strong>Filtering</strong> phase. If a Node doesn't match, it is eliminated.</li>
</ol>
<pre><code class="language-yaml">spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: disktype
            operator: In
            values:
            - nvme
</code></pre>
<ol>
<li><code>preferredDuringSchedulingIgnoredDuringExecution</code> <strong>(The Soft Rule)</strong> Evaluated during the <strong>Scoring</strong> phase. The Scheduler gives extra points to Nodes with matching labels. However, if no perfect nodes are available, the Scheduler will still schedule the Pod elsewhere rather than leaving it <code>Pending</code>.</li>
</ol>
<p><strong>What does</strong> <code>IgnoredDuringExecution</code> <strong>mean?</strong> Suppose a Pod requires an <code>nvme</code> node and is successfully scheduled there. A week later, an administrator removes the <code>disktype: nvme</code> label from that Node. What happens? <em>Nothing.</em> The rule is ignored during execution. The Pod will continue running happily.</p>
<h2>5. Deep Dive: Taints and Tolerations (Repelling Pods)</h2>
<p>If Affinity is a magnet that attracts Pods, Taints are a repellent that pushes them away.</p>
<p>Think of a Node as a <strong>restricted machine pool</strong>. When you add a <strong>Taint</strong> to that Node (for example, <code>dedicated=gpu:NoSchedule</code>), Kubernetes treats it as protected and avoids placing regular Pods there. Only Pods with the correct <strong>Toleration</strong> are considered eligible to run on it.</p>
<p>In simple terms: <strong>Taint protects the node, and Toleration gives a Pod permission to be placed there.</strong></p>
<pre><code class="language-yaml">spec:
  tolerations:
  - key: "dedicated"
    operator: "Equal"
    value: "gpu"
    effect: "NoSchedule"
</code></pre>
<p>Where people get confused is the <strong>Effect</strong>.</p>
<ol>
<li><p><code>NoSchedule</code><strong>:</strong> "Do not schedule any <em>new</em> Pods here unless they tolerate this taint." Evaluated during the <strong>Filtering</strong> phase. <strong>Edge Case:</strong> If you add this taint to a Node already running 50 normal Pods, those 50 Pods will continue to run perfectly fine. Only future deployments will be blocked.</p>
</li>
<li><p><code>PreferNoSchedule</code><strong>:</strong> A soft rule. The Scheduler will try its best not to put new Pods on this Node, but if the cluster is completely full, it will bypass the restriction. Evaluated during the <strong>Scoring</strong> phase.</p>
</li>
<li><p><code>NoExecute</code> <strong>(The Dangerous One):</strong> This is an aggressive rule used during runtime. If you apply a <code>NoExecute</code> taint to a Node, it blocks new Pods <strong>AND immediately evicts any existing running Pods</strong> that lack the matching toleration.</p>
</li>
</ol>
<h3>Quick Summary: Filtering vs Scoring Map</h3>
<p>To keep your mental model clean, here is exactly where each concept fits:</p>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Phase</th>
<th>Purpose</th>
</tr>
</thead>
<tbody><tr>
<td><code>requests</code></td>
<td>Filtering</td>
<td>Node must have enough CPU/Mem.</td>
</tr>
<tr>
<td><code>nodeSelector</code></td>
<td>Filtering</td>
<td>Node must have exact label.</td>
</tr>
<tr>
<td><code>required</code> <strong>Affinity</strong></td>
<td>Filtering</td>
<td>Node must match hard rules.</td>
</tr>
<tr>
<td><code>NoSchedule</code> <strong>Taint</strong></td>
<td>Filtering</td>
<td>Pod must have matching toleration.</td>
</tr>
<tr>
<td><code>preferred</code> <strong>Affinity</strong></td>
<td>Scoring</td>
<td>Node gets bonus points if it matches.</td>
</tr>
<tr>
<td><code>limits</code></td>
<td>Neither</td>
<td>Enforced at runtime by <code>kubelet</code>.</td>
</tr>
<tr>
<td><strong>Health Probes</strong></td>
<td>Neither</td>
<td>Executed at runtime by <code>kubelet</code>.</td>
</tr>
</tbody></table>
<hr />
<h2>6. Bypassing the Scheduler: Manual and Static Pods</h2>
<p>What if you don't want to use the Two-Phase process?</p>
<p><strong>Manual Scheduling</strong> You can bypass the Scheduler entirely by hardcoding the <code>nodeName</code> directly in your YAML:</p>
<pre><code class="language-yaml">spec:
  nodeName: worker-node-01
  containers:
  - name: my-app
    image: nginx
</code></pre>
<p>The API Server skips the Scheduler entirely. The <code>kubelet</code> on <code>worker-node-01</code> simply sees the assignment and starts the Pod.</p>
<p><strong>Static Pods</strong> Static Pods bypass the API Server and the Scheduler entirely. If you SSH into a Worker Node, you will find a specific directory configured in the kubelet (usually <code>/etc/kubernetes/manifests</code>). If you drop a standard Pod YAML file into this folder, the local <code>kubelet</code> detects the file and starts the Pod directly.</p>
<p><strong>What happens if you edit that file?</strong> The <code>kubelet</code> continuously watches that directory on the hard drive. If you open <code>pod.yaml</code> in vim and change the image version, the <code>kubelet</code> instantly notices the file modification, cleanly shuts down the running container, and restarts it with the new configuration. If you delete the file, the Pod is killed.</p>
<p><em>Why does this exist?</em> This is exactly how Kubernetes runs its own control plane! The API Server, Scheduler, and etcd are all run as Static Pods on the master nodes, allowing them to boot up before the Kubernetes API even exists.</p>
<h2>7. Runtime: The Kubelet and Health Probes</h2>
<p>Once the Pod is placed on a Node, the <code>kubelet</code> takes ownership. To know if your application is actually working, the <code>kubelet</code> relies on Health Probes. (Remember our table: The Scheduler never looks at Health Probes).</p>
<pre><code class="language-yaml">spec:
  containers:
  - name: my-app
    image: my-app:v1
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
    readinessProbe:
      httpGet:
        path: /ready
        port: 8080
</code></pre>
<p><strong>1. Liveness Probe ("Is the app dead?")</strong> If the app returns an error or times out, the <code>kubelet</code> assumes the container has crashed, kills it, and restarts it.</p>
<p><strong>2. Readiness Probe ("Can it handle traffic?")</strong> If this probe fails, the container is <em>not</em> killed. Instead, Kubernetes temporarily removes the Pod's IP address from the Service endpoints. <strong>The Edge Case:</strong> If your Node.js app loses its connection to the database, a failing Liveness probe would trap your app in an infinite reboot loop. Instead, a failing <em>Readiness</em> probe simply stops user traffic from reaching that Pod, leaving the container running peacefully until the DB comes back online.</p>
<p><strong>3. Startup Probe</strong> For legacy applications that take 30 seconds to boot, a strict Liveness probe might kill the app before it ever finishes starting. A Startup probe pauses the other probes until the application successfully boots for the first time.</p>
<h2>8. Day 2 Operations: Autoscaling</h2>
<p>Now your Pods are healthy, but what happens when traffic spikes by 1000%?</p>
<p><strong>1. Horizontal Pod Autoscaler (HPA)</strong> The HPA scales the <em>number</em> of Pod replicas. It watches metrics (like CPU).</p>
<pre><code class="language-yaml">apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: frontend-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: frontend
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50
</code></pre>
<p>If your Pods hit 80% CPU, the HPA dynamically alters your Deployment from 2 Pods up to 10.</p>
<p><strong>2. Vertical Pod Autoscaler (VPA)</strong> The VPA scales the <em>size</em> of your Pods. It analyzes historical usage and adjusts the CPU/Memory <code>requests</code> and <code>limits</code>. <strong>The Gotcha:</strong> Never use HPA and VPA on the same metric (like CPU). The HPA will try to add more Pods, while the VPA will try to reboot existing Pods with higher limits. They will fight each other, causing massive instability.</p>
<p><strong>3. Cluster Autoscaler (CA)</strong> If the HPA requests 20 new Pods but your Worker Nodes are completely out of CPU, those new Pods will sit in <code>Pending</code>. The Cluster Autoscaler watches for <code>Pending</code> Pods and makes API calls to AWS/GCP to spin up brand new Virtual Machines to accommodate them.</p>
<h3>The Modern Standard: KEDA</h3>
<p>Standard HPA relies heavily on CPU and Memory. But what if you are processing a message queue? Your CPU might be 10%, but you have 10,000 messages waiting in AWS SQS.</p>
<p>Enter <strong>KEDA (Kubernetes Event-driven Autoscaling)</strong>. KEDA can talk directly to external systems like Kafka or AWS SQS.</p>
<pre><code class="language-yaml">apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: aws-sqs-scaler
spec:
  scaleTargetRef:
    name: worker-deployment
  minReplicaCount: 0   # Scale to zero!
  maxReplicaCount: 10
  triggers:
  - type: aws-sqs-queue
    metadata:
      queueURL: https://sqs.us-east-1.amazonaws.com/123/my-queue
      queueLength: "100" # Add 1 Pod for every 100 messages
</code></pre>
<p>KEDA translates these external events into metrics the standard HPA can understand, enabling true event-driven, scale-to-zero architectures.</p>
<hr />
<p>You now understand the complete lifecycle of a workload in Kubernetes. You write declarative YAML. The API server stores it. The Scheduler filters and scores your nodes, respecting your Taints and Affinities. The <code>kubelet</code> takes over, starting your containers and running Health Probes. Finally, components like the HPA and Cluster Autoscaler continuously monitor and adjust the scale of your infrastructure to match reality.</p>
<p>In the next part of this series, we will look at how to package all these complex YAML files into something reusable using <strong>Helm</strong>, and explore how to manage production-ready configurations and secrets securely.</p>
<p>Hope you found this useful!<br />Reach out to me on <a href="https://x.com/SagnikGuru">Twitter</a> and/or <a href="https://www.linkedin.com/in/sagnik-guru-46790919a">LinkedIn</a>.</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[Kubernetes Fundamentals, Part 3: Services, Networking, and External Access]]></title><description><![CDATA[In Part 1, we looked at how the Kubernetes cluster is built. In Part 2, we learned how to define and deploy our applications using YAML, Deployments, and Volumes.
At this point, we have Pods running i]]></description><link>https://kubernetes-sagnik.hashnode.dev/kubernetes-fundamentals-part-3-services-networking-and-external-access</link><guid isPermaLink="true">https://kubernetes-sagnik.hashnode.dev/kubernetes-fundamentals-part-3-services-networking-and-external-access</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Sagnik Guru]]></dc:creator><pubDate>Sun, 22 Mar 2026 17:06:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6802f5e18e6fd72ba8c17773/b36845ca-ee43-4931-af9b-96601bb3d6fd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In Part 1, we looked at how the Kubernetes cluster is built. In Part 2, we learned how to define and deploy our applications using YAML, Deployments, and Volumes.</p>
<p>At this point, we have Pods running inside our cluster. But right now, they are completely isolated. The frontend cannot talk to the backend, the backend cannot reach the database, and users on the internet cannot access the application at all.</p>
<p>If you come from a traditional Virtual Machine (VM) or pure Docker background, Kubernetes networking can feel like black magic. Today, we are going to demystify it. By the end of this article, you will understand exactly how traffic flows inside a cluster, how to expose your applications to the world, and how to do it securely and efficiently.</p>
<h2>The Foundation: Pod Networking</h2>
<p>Before we can connect our applications, we need to understand how the network works at the lowest level. Kubernetes networking is built on a few strict rules. If you understand these, the rest of the system makes sense.</p>
<h3>1. Containers in the same Pod (The "Pause" Container)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6802f5e18e6fd72ba8c17773/7b5e975e-667a-4b1b-9803-fe8526b253f0.png" alt="" style="display:block;margin:0 auto" />

<p>In standard Docker, every container gets its own isolated network interface and IP address. Kubernetes does things differently. Instead of giving each container an IP, it groups them into a Pod and gives the <strong>Pod</strong> the IP address.</p>
<p>To make this work, Kubernetes creates a tiny, invisible container called the "pause" container before it starts your actual application. This container does nothing but hold a network namespace open. Your application containers then join this shared space.</p>
<p>Because they share the exact same network stack, if you have a Node.js container and a Redis container in the same Pod, Node.js can connect to Redis simply by calling <code>localhost:6379</code>.</p>
<h3>2. Pods on the same Node</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6802f5e18e6fd72ba8c17773/9a236f0e-3c13-48d6-bb70-eb7a68f03b28.png" alt="" style="display:block;margin:0 auto" />

<p>Every Pod gets its own cluster-wide unique IP address. Let's say Pod A (<code>10.1.0.2</code>) and Pod B (<code>10.1.0.3</code>) are running on the same physical worker node.</p>
<p>When Pod A wants to talk to Pod B, the traffic flows out of Pod A's virtual ethernet interface, hits a virtual network bridge inside the Linux kernel of that node, and is immediately handed over to Pod B's interface. It is fast, local, and requires no complex routing.</p>
<h3>3. Pods on different Nodes (The Overlay Network)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6802f5e18e6fd72ba8c17773/2777d509-f7c9-4951-8d65-e0ecafc470a5.png" alt="" style="display:block;margin:0 auto" />

<p>In this scenario, we have two worker nodes sitting on a standard corporate physical network (VLAN/Subnet <code>192.168.1.0/24</code>).</p>
<p><strong>The Environment:</strong></p>
<ul>
<li><p><strong>Worker Node 1:</strong> Physical IP <code>192.168.1.10</code> | Pod CIDR <code>10.1.0.0/24</code></p>
</li>
<li><p><strong>Worker Node 2:</strong> Physical IP <code>192.168.1.11</code> | Pod CIDR <code>10.2.0.0/24</code></p>
</li>
<li><p><strong>Source (Pod A on Node 1):</strong> IP <code>10.1.0.5</code></p>
</li>
<li><p><strong>Destination (Pod C on Node 2):</strong> IP <code>10.2.0.5</code></p>
</li>
</ul>
<h4>The Step-by-Step Execution</h4>
<ol>
<li><p><strong>Packet Initiation:</strong></p>
<p>Pod A (<code>10.1.0.5</code>) generates a network packet. The "Inner Header" lists the Source as <code>10.1.0.5</code> and the Destination as <code>10.2.0.5</code>.</p>
</li>
<li><p><strong>Routing Decision (Node 1):</strong></p>
<p>The packet hits the Virtual Ethernet interface on Node 1. The Container Network Interface (CNI)—such as Flannel or Calico—checks the local routing table. It identifies that the <code>10.2.0.0/24</code> range is hosted by <strong>Worker Node 2</strong> at physical address <code>192.168.1.11</code>.</p>
</li>
<li><p><strong>Encapsulation (The "Envelope"):</strong></p>
<p>Since the physical network switches don't know where <code>10.2.0.5</code> is, Node 1 wraps the original packet inside a new one (typically using <strong>VXLAN</strong> or <strong>UDP</strong>).</p>
<ul>
<li><p><strong>Outer Source IP:</strong> <code>192.168.1.10</code> (Node 1)</p>
</li>
<li><p><strong>Outer Destination IP:</strong> <code>192.168.1.11</code> (Node 2)</p>
</li>
<li><p><strong>Payload:</strong> The original Pod A -&gt; Pod C packet.</p>
</li>
</ul>
</li>
<li><p><strong>Physical Transit:</strong></p>
<p>The physical network equipment (routers/switches) sees a standard packet moving from <code>192.168.1.10</code> to <code>192.168.1.11</code>. It routes this efficiently across the data center.</p>
</li>
<li><p><strong>Decapsulation (Node 2):</strong></p>
<p>Worker Node 2 receives the packet on its physical interface. It recognizes the encapsulation header, strips it off, and reveals the original "Inner" packet destined for <code>10.2.0.5</code>.</p>
</li>
<li><p><strong>Final Delivery:</strong></p>
<p>Node 2 looks at its local bridge and sees that Pod C is attached to its local interface. It delivers the packet directly to Pod C's network namespace.</p>
</li>
</ol>
<h2>The Core Problem: Ephemeral Pods</h2>
<p>Now that we know Pods can talk to each other via IP addresses, we run into a major real-world problem.</p>
<p>Let's imagine a standard architecture: a React Frontend talking to a Node.js Backend. In a VM world, you would hardcode the backend server's IP address (like <code>192.168.1.50</code>) into your frontend code, and it would work forever.</p>
<p>In Kubernetes, Pods are ephemeral. If a Node.js backend Pod crashes, or you deploy a new version, Kubernetes destroys the old Pod and creates a new one. The new Pod will get a completely different, random IP address.</p>
<p>If your frontend is trying to send API requests to <code>10.1.0.2</code>, and that Pod dies and is replaced by a Pod at <code>10.2.0.9</code>, your application breaks.</p>
<p>You can never rely on a Pod's IP address. We need a stable networking layer.</p>
<h2>The Solution: Kubernetes Services</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6802f5e18e6fd72ba8c17773/c6af3bff-86ad-450b-bc7f-91f8c76871df.png" alt="" style="display:block;margin:0 auto" />

<p>To solve the changing IP problem, Kubernetes introduces the concept of a <strong>Service</strong>.</p>
<p>Think of Pods like temporary workers in a call center, and a Service like the company’s main phone number. Workers might quit, change desks, or go on break, but the customer always dials the same 1-800 number, and the call gets routed to whoever is currently available.</p>
<p>A Service gives you a stable virtual IP address, a permanent DNS name, and built-in load balancing across your Pods.</p>
<p>There are four types of Services you need to know.</p>
<h3>1. ClusterIP (The Default)</h3>
<p><code>ClusterIP</code> creates a stable, virtual IP address that is only reachable from inside the cluster. This is what you use for internal application-to-application traffic.</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: backend-service
spec:
  type: ClusterIP
  selector:
    app: backend
  ports:
    - port: 80
      targetPort: 3000
</code></pre>
<p>When your frontend makes an HTTP request to <code>backend-service</code> on port <code>80</code>, the Service catches the traffic and load-balances it across whichever backend Pods are currently alive.</p>
<h3>2. NodePort (For Local Testing)</h3>
<p>If you want to access your application from outside the cluster, you can use a NodePort. A NodePort opens a specific port (between 30000 and 32767) on the actual, physical IP address of <strong>every single worker node</strong> in your cluster.</p>
<pre><code class="language-yaml">  type: NodePort
  ports:
    - port: 80
      targetPort: 3000
      nodePort: 30080
</code></pre>
<p>If you visit <code>http://&lt;Node-IP&gt;:30080</code> in your browser, the node receives the traffic and forwards it to your Pods.</p>
<p><strong>Why NodePort is a bad approach for production:</strong> While NodePort is great for quick debugging, you should almost never use it in production. It forces you to manage a messy range of high-number ports. More importantly, it requires you to expose your underlying infrastructure (the node IPs) to external traffic, bypassing standard security firewalls and cloud load balancers.</p>
<h3>3. LoadBalancer (The Cloud Way)</h3>
<p>To properly expose an application to the internet, you use the <code>LoadBalancer</code> type.</p>
<p>When you deploy a <code>LoadBalancer</code> Service, Kubernetes talks directly to your cloud provider (AWS, GCP, Azure). The cloud provider automatically spins up a physical, external Load Balancer, assigns it a secure public IP address, and wires it up to your cluster. This is the standard way to accept external internet traffic.</p>
<h3>4. ExternalName (The DNS Trick)</h3>
<p>Suppose your database is not running inside Kubernetes, but is instead a managed service like AWS RDS (<code>mydb.abc123.rds.amazonaws.com</code>). You could hardcode that URL into your application, but if the database URL ever changes, you have to rewrite your code.</p>
<p>Instead, you create an <code>ExternalName</code> service:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: postgres-db
spec:
  type: ExternalName
  externalName: mydb.abc123.rds.amazonaws.com
</code></pre>
<p>This service does no routing and has no IP. It is simply a DNS alias inside the cluster. Your backend application simply connects to <code>postgres-db</code>, and Kubernetes translates that on the fly to the AWS URL.</p>
<p><em>Note: Where does the database port (like 5432) come from? The port comes from your application code! Your backend says</em> <code>connect("postgres-db", 5432)</code><em>. Kubernetes translates the hostname, but the port stays exactly what your code requested.</em></p>
<h2>Clarifying Ports: port vs targetPort vs nodePort</h2>
<p>When writing Service YAML, this is the most common place developers make mistakes. Let's define them clearly:</p>
<ul>
<li><p><code>targetPort</code>: The port your actual application is running on inside the container (e.g., your Node.js app is listening on <code>3000</code>).</p>
</li>
<li><p><code>port</code>: The port the <em>Service</em> exposes inside the cluster. Other Pods will use this port to talk to the Service. (e.g., setting this to <code>80</code> means other apps can just call <code>http://backend-service</code> without typing a port).</p>
</li>
<li><p><code>nodePort</code>: The port opened on the physical worker nodes (only used if the Service type is NodePort).</p>
</li>
</ul>
<h2>Under the Hood: How Services Actually Work (kube-proxy)</h2>
<p>We know Services provide a stable IP, but here is the biggest secret in Kubernetes: <strong>A Service is not a real thing.</strong></p>
<p>There is no "Service container" running anywhere. A Service does not have a network interface. A Service's IP address is entirely fake. It exists only as a set of rules programmed into the kernel of your worker nodes.</p>
<p>The component responsible for this is called <strong>kube-proxy</strong>. Despite its name, <code>kube-proxy</code> is not a traditional proxy like NGINX. It does not sit in the middle and catch traffic. Instead, <code>kube-proxy</code> is an agent that runs on every single node. It watches the Kubernetes API, and whenever a Service is created, it writes <code>iptables</code> or <code>IPVS</code> rules directly into the Linux kernel of its node.</p>
<h3>The Packet Flow</h3>
<p>Let's trace what happens when Pod A (on Node 1) calls a ClusterIP Service that points to Pod C (on Node 2).</p>
<ol>
<li><p><strong>The Request:</strong> Pod A makes a request to the Service's virtual IP (<code>10.96.0.10</code>).</p>
</li>
<li><p><strong>The Kernel Intercepts:</strong> The packet leaves Pod A and hits the Linux kernel of Node 1.</p>
</li>
<li><p><strong>The Rule Match:</strong> The rules created by <code>kube-proxy</code> immediately spot the destination IP (<code>10.96.0.10</code>).</p>
</li>
<li><p><strong>DNAT (Destination NAT):</strong> The kernel mathematically picks a healthy backend Pod (Pod C). It then rewrites the packet's destination IP, changing it from the fake Service IP to the real IP of Pod C (<code>10.2.0.5</code>).</p>
</li>
<li><p><strong>Normal Routing:</strong> Now that the packet has a real Pod IP, it leaves Node 1, travels across the network to Node 2, and is delivered to Pod C.</p>
</li>
</ol>
<p><strong>The critical takeaway:</strong> The decision of <em>which</em> Pod to send the traffic to, and the rewriting of the IP address, happens entirely on the <strong>source node</strong> (Node 1). By the time the packet reaches Node 2, it is just a normal packet aimed at a normal Pod.</p>
<h2>Exposing to the World: Ingress vs LoadBalancer</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6802f5e18e6fd72ba8c17773/4a889831-e1c0-4cbe-bf44-81a3a5fea170.png" alt="" style="display:block;margin:0 auto" />

<p>We know that <code>LoadBalancer</code> Services expose applications to the internet. But what if you have a microservices architecture with a Frontend UI, a User API, and a Payment API?</p>
<p>If you use <code>LoadBalancer</code> Services, AWS will create three separate external load balancers. You will have three separate public IP addresses, and you will pay the monthly fee for all three. Furthermore, standard Load Balancers operate at Layer 4 (TCP). They do not understand HTTP URLs. You cannot tell an AWS Load Balancer to route <code>/api</code> to one place and <code>/frontend</code> to another.</p>
<h3>Enter Ingress: The Smart Router</h3>
<p>To solve this, Kubernetes provides the <strong>Ingress</strong>.</p>
<p>An Ingress operates at Layer 7 (HTTP/HTTPS). Instead of creating a LoadBalancer for every single microservice, you deploy one single <strong>Ingress Controller</strong> (usually an NGINX proxy) and expose it via one <code>LoadBalancer</code> Service.</p>
<p>You then write Ingress rules to organize your traffic based on URLs:</p>
<pre><code class="language-yaml">apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
spec:
  rules:
    - host: myapp.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: frontend-service
                port:
                  number: 80
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: backend-service
                port:
                  number: 80
</code></pre>
<p>By using an Ingress, your traffic flow becomes highly efficient:</p>
<ol>
<li><p><strong>Massive Cost Savings:</strong> You only pay for one cloud Load Balancer.</p>
</li>
<li><p><strong>Smart Routing:</strong> Traffic to <code>myapp.com/api</code> goes to the backend, and <code>/</code> goes to the frontend.</p>
</li>
<li><p><strong>Centralized SSL:</strong> You can attach your SSL/TLS certificates directly to the Ingress, meaning your internal Pods can communicate over simple HTTP without worrying about encryption overhead.</p>
</li>
</ol>
<p>A LoadBalancer opens a door to your cluster. An Ingress stands right inside that door, reads the URL of every request, and smartly routes it to the correct internal Service.</p>
<hr />
<p>You now have a complete mental model of Kubernetes networking. You know that Pods share network namespaces via the pause container, and that they communicate across a flat, NAT-free network. You know how <code>kube-proxy</code> translates fake Service IPs into real Pod IPs directly in the kernel. And you know why Ingress is the industry standard for routing external HTTP traffic.</p>
<p>At this point, we know how to configure our workloads, give them storage, and connect them to the network. But there is one major piece of the puzzle left: when you deploy a Pod, how does Kubernetes decide <em>which</em> worker node it should actually run on?</p>
<p>In the next part of this series, we will dive into the <strong>Kubernetes Scheduler</strong> to understand node selection, hardware constraints, and how to control exactly where your applications live.</p>
<p>Hope you found this useful!<br />Reach out to me on <a href="https://x.com/SagnikGuru">Twitter</a> and/or <a href="https://www.linkedin.com/in/sagnik-guru-46790919a">LinkedIn</a>.</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[Kubernetes Fundamentals, Part 2: From YAML to Real Workloads]]></title><description><![CDATA[In Part 1, we looked at the high-level side of Kubernetes, why it exists, the main components involved, and the big-picture flow of how a cluster works.
In this part, we’ll move from theory to actual ]]></description><link>https://kubernetes-sagnik.hashnode.dev/kubernetes-fundamentals-part-2-from-yaml-to-real-workloads</link><guid isPermaLink="true">https://kubernetes-sagnik.hashnode.dev/kubernetes-fundamentals-part-2-from-yaml-to-real-workloads</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Sagnik Guru]]></dc:creator><pubDate>Sun, 22 Mar 2026 10:06:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6802f5e18e6fd72ba8c17773/3eede6f7-6556-4a29-9bb2-dc693d6d2cb3.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In Part 1, we looked at the high-level side of Kubernetes, why it exists, the main components involved, and the big-picture flow of how a cluster works.</p>
<p>In this part, we’ll move from theory to actual usage. The goal is simpler than that: if you are a developer who needs to work with Kubernetes in a project, this should give you the practical foundation to read configs, understand deployments, and not feel lost when you see a real cluster setup.</p>
<h1>Imperative vs Declarative</h1>
<p>Before we start creating things in Kubernetes, there’s one important idea to understand: there are generally two ways to work with it.</p>
<h2>Imperative way</h2>
<p>This is the command-driven approach. You directly tell Kubernetes what to do.</p>
<pre><code class="language-shell">kubectl run my-pod --image=nginx
kubectl create deployment my-app --image=nginx
kubectl scale deployment my-app --replicas=3
</code></pre>
<p>This style is quick and useful for testing, debugging, or learning. If you just want to spin something up and see what happens, imperative commands are convenient.</p>
<p>But the downside is that commands are not a great long-term source of truth. Once you run them, the cluster changes, but you do not have a clear config file that documents what your system is supposed to look like.</p>
<h2>Declarative way</h2>
<p>This is the file-based approach(YAML is the preferred way). Instead of telling Kubernetes step by step what to do, you describe the desired state in a file and apply it.</p>
<pre><code class="language-bash">kubectl apply -f pod.yaml
kubectl apply -f deployment.yaml
</code></pre>
<p>That YAML file becomes your source of truth. It can be stored in Git, reviewed by your team, reused in different environments, and updated over time.</p>
<p>In real projects, this is the approach you’ll use most of the time. So from this point onward, we’ll mostly think in the declarative way.</p>
<h1>Creating a cluster</h1>
<p>Before creating Pods or Deployments, we need a cluster.</p>
<p>In managed cloud platforms, you usually create Kubernetes clusters through a UI, CLI, or infrastructure tools. For example, in AWS, GCP, or Azure, the cloud provider gives you ways to define the cluster and the worker nodes you want. Under the hood, you are still describing the shape of the cluster — just often through provider-specific tools.</p>
<p>For learning locally, one of the easiest options is <strong>kind</strong>, which stands for Kubernetes in Docker. It lets you create a local multi-node Kubernetes cluster using a YAML file.</p>
<p>Here is a simple example:</p>
<pre><code class="language-yaml">kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
  - role: worker
</code></pre>
<p>And then you create it with:</p>
<pre><code class="language-bash">kind create cluster --config kind-config.yaml
</code></pre>
<p>This gives you:</p>
<ul>
<li><p>1 control plane node</p>
</li>
<li><p>2 worker nodes</p>
</li>
</ul>
<p>That is enough to simulate a more realistic setup than a single-node cluster.</p>
<p>If you are using a managed Kubernetes platform, the exact YAML and commands will differ, but the idea stays similar: you define the structure you want, and the platform creates the cluster for you.</p>
<h1>Understanding Kubernetes YAML</h1>
<p>Now let’s talk about the most common thing you’ll see in Kubernetes: YAML files.</p>
<p>Almost every Kubernetes resource is defined using the same broad structure:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
    - name: nginx
      image: nginx
</code></pre>
<p>At a high level, the main parts are:</p>
<h3><code>apiVersion</code></h3>
<p>This tells Kubernetes which API version should handle this resource.</p>
<h3><code>kind</code></h3>
<p>This tells Kubernetes what you are creating. It could be a Pod, Deployment, Service, ConfigMap, Secret, and so on.</p>
<h3><code>metadata</code></h3>
<p>This contains identifying information such as the resource name, labels, and namespace.</p>
<h3><code>spec</code></h3>
<p>This is the most important section. It describes the desired state of the resource. In other words, this is where you define what you actually want Kubernetes to create and maintain.</p>
<h1>Writing your first Pod</h1>
<p>The first real workload to understand in Kubernetes is a <strong>Pod</strong>.</p>
<p>Here is a basic Pod YAML:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: my-app-pod
  labels:
    app: my-app
spec:
  containers:
    - name: my-app-container
      image: nginx:latest
      ports:
        - containerPort: 80
</code></pre>
<p>You can create it using:</p>
<pre><code class="language-bash">kubectl apply -f pod.yaml
</code></pre>
<p>Then inspect it with:</p>
<pre><code class="language-bash">kubectl get pods
kubectl describe pod my-app-pod
</code></pre>
<h3>Important parts of a Pod spec</h3>
<p>In a real project, some fields you’ll commonly work with are:</p>
<ul>
<li><p><code>containers</code>: The list of containers inside the Pod.</p>
</li>
<li><p><code>image</code>: The container image to run.</p>
</li>
<li><p><code>ports</code>: Which ports the container exposes.</p>
</li>
<li><p><code>env</code>: Environment variables.</p>
</li>
<li><p><code>resources</code>: CPU and memory requests/limits.</p>
</li>
<li><p><code>volumeMounts</code>: Where storage is mounted inside the container.</p>
</li>
<li><p><code>volumes</code>: Storage definitions used by the Pod.</p>
</li>
</ul>
<p>A slightly more realistic example:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: app-pod
  labels:
    app: app-pod
spec:
  containers:
    - name: app-container
      image: nginx:latest
      ports:
        - containerPort: 80
      env:
        - name: APP_ENV
          value: "development"
      resources:
        requests:
          cpu: "100m"
          memory: "128Mi"
        limits:
          cpu: "250m"
          memory: "256Mi"
</code></pre>
<p>At this point, you do not need to master every field. The main thing is to start recognizing the structure and understand that a Pod spec is just a description of how a containerized workload should run.</p>
<h3>Why a plain Pod is not enough</h3>
<p>A Pod is fine for learning, testing, or one-off workloads.</p>
<p>But in real applications, a plain Pod has a problem: if that Pod dies, Kubernetes does not automatically guarantee that a new identical one will continue running just because you wanted one. That is where higher-level controllers come in.</p>
<p>And that leads us to ReplicaSets.</p>
<h1>Why ReplicaSets exist</h1>
<p>A ReplicaSet exists to make sure a certain number of Pod replicas are always running.</p>
<p>If you say you want 3 Pods, the ReplicaSet keeps checking the cluster and tries to maintain exactly 3. If one crashes or gets deleted, it creates another one.</p>
<p>Here is a simple ReplicaSet:</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: my-app-rs
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: nginx-container
          image: nginx:latest
          ports:
            - containerPort: 80
</code></pre>
<p>A few important things to notice here:</p>
<ul>
<li><p><code>replicas: 3</code> means Kubernetes should keep 3 Pods running.</p>
</li>
<li><p><code>selector.matchLabels</code> tells the ReplicaSet which Pods belong to it.</p>
</li>
<li><p><code>template</code> contains the Pod definition that should be created.</p>
</li>
</ul>
<p>That <code>template</code> section is basically a Pod spec embedded inside another resource.</p>
<h3>What actually happens</h3>
<p>Suppose one of the Pods managed by this ReplicaSet gets deleted.</p>
<p>The ReplicaSet notices that the actual number of running Pods is now lower than the desired number. So it creates a new Pod to fix that mismatch.</p>
<h1>Deployments</h1>
<p>A Deployment sits one level above ReplicaSets.</p>
<p>It manages ReplicaSets for you and gives you better control over updates, rollbacks, and rollout history. If ReplicaSet is about “keep these Pods alive,” Deployment is about “manage this application properly over time.”</p>
<p>Here’s a basic Deployment:</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: app
          image: nginx:1.25
          ports:
            - containerPort: 80
</code></pre>
<p>If later you change the image version:</p>
<pre><code class="language-yaml">image: nginx:1.26
</code></pre>
<p>and apply the file again, Kubernetes performs a rolling update. Instead of killing all old Pods at once and creating all new ones at once, it gradually replaces them so the application keeps running during the update.</p>
<p>Useful commands here are:</p>
<pre><code class="language-shell">kubectl apply -f deployment.yaml
kubectl get deployments
kubectl rollout status deployment/app-deployment
kubectl rollout history deployment/app-deployment
kubectl rollout undo deployment/app-deployment
</code></pre>
<p>This is why Deployments are the standard way to run stateless applications in Kubernetes. They give you scaling, self-healing, and controlled updates in one place.</p>
<h3>DaemonSets</h3>
<p>A DaemonSet is different from a Deployment.</p>
<p>A Deployment runs a chosen number of replicas, but a DaemonSet ensures that a specific Pod runs on every matching node. If a new node joins the cluster, Kubernetes automatically creates that Pod there too.</p>
<p>This is useful for workloads that should exist once per node rather than a fixed number across the cluster.</p>
<p>Common examples include:</p>
<ul>
<li><p>Log collection agents.</p>
</li>
<li><p>Monitoring agents.</p>
</li>
<li><p>Node-level security tools.</p>
</li>
<li><p>Network-related agents.</p>
</li>
</ul>
<p>A simple DaemonSet looks like this:</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-agent
spec:
  selector:
    matchLabels:
      app: node-agent
  template:
    metadata:
      labels:
        app: node-agent
    spec:
      containers:
        - name: agent
          image: nginx:latest
</code></pre>
<p>So the easiest way to remember it is:</p>
<ul>
<li><p><strong>Deployment</strong>: “Run 3 copies of this app.”</p>
</li>
<li><p><strong>DaemonSet</strong>: “Run 1 copy of this app on every node.”</p>
</li>
</ul>
<h2>Storage and persistence</h2>
<h3>Why volumes matter</h3>
<p>Containers are meant to be lightweight and replaceable. That is great for scaling and recovery, but it creates an important problem: data written inside the container filesystem is usually lost if the container is recreated.</p>
<p>That means if your application uploads files, stores local state, or depends on persistent data, you need a better storage model.</p>
<p>This is where volumes come in.</p>
<h3>Basic volume types</h3>
<p>To use storage in Kubernetes, it is always a two-step process:</p>
<ol>
<li><p>You define the storage at the <strong>Pod level</strong> (using <code>volumes</code>).</p>
</li>
<li><p>You plug that storage into your <strong>container</strong> at a specific folder path (using <code>volumeMounts</code>).</p>
</li>
</ol>
<p>One basic example is <code>emptyDir</code>:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: volume-demo
spec:
  containers:
    - name: app
      image: nginx
      volumeMounts:
        - name: temp-storage
          mountPath: /usr/share/nginx/html
  volumes:
    - name: temp-storage
      emptyDir: {}
</code></pre>
<p>If you look closely at the YAML, the <code>volumes</code> section at the bottom tells Kubernetes: <em>"<em>Create a temporary empty directory and name it <code>temp-storage</code>.</em>"</em></p>
<p>Then, the <code>volumeMounts</code> section inside the container tells Kubernetes: *"*Take that <code>temp-storage</code> volume we just created and make it accessible inside this container at the path <code>/usr/share/nginx/html</code>."</p>
<p>An <code>emptyDir</code> is created when the Pod starts and exists only as long as the Pod exists. It is useful for temporary scratch space or sharing files between multiple containers in the same Pod, but it is not meant for persistent application data.</p>
<p>Another option is <code>hostPath</code>, which mounts a directory directly from the underlying worker node's filesystem:</p>
<pre><code class="language-yaml">volumes:
  - name: host-storage
    hostPath:
      path: /data/app
      type: DirectoryOrCreate #Look for this folder path on the node. If it already exists, use it. If it does not exist, create an empty directory at that path first, and then use it.
</code></pre>
<p>This can be useful in local development setups (like <code>kind</code> or Minikube) or for debugging, but it is usually not the right choice for production applications because it permanently ties your Pod's data to one specific physical node.</p>
<h1>Persistent Volumes and Persistent Volume Claims</h1>
<p>For real persistence, Kubernetes uses <strong>Persistent Volumes (PV)</strong> and <strong>Persistent Volume Claims (PVC)</strong>.</p>
<p>The easiest way to think about them is this:</p>
<ul>
<li><p>A <strong>Persistent Volume</strong> is the actual storage resource.</p>
</li>
<li><p>A <strong>Persistent Volume Claim</strong> is a request for storage made by an application.</p>
</li>
</ul>
<p>Your Pod does not usually ask for a disk directly. Instead, it asks for a PVC, and that PVC gets bound to a PV.</p>
<p>Here’s a simple PVC:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
</code></pre>
<p>And here’s how a Pod uses it:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: app-with-storage
spec:
  containers:
    - name: app
      image: nginx
      volumeMounts:
        - name: app-storage
          mountPath: /data
  volumes:
    - name: app-storage
      persistentVolumeClaim:
        claimName: app-pvc
</code></pre>
<p>Now the Pod is using persistent storage through the claim.</p>
<h3>Access modes</h3>
<p>When working with persistent storage, you’ll often see these access modes:</p>
<ul>
<li><p><code>ReadWriteOnce (RWO)</code> — mounted as read-write by a single node.</p>
</li>
<li><p><code>ReadOnlyMany (ROX)</code> — mounted as read-only by many nodes.</p>
</li>
<li><p><code>ReadWriteMany (RWX)</code> — mounted as read-write by many nodes.</p>
</li>
</ul>
<p>These matter because not all storage backends support all modes. Think about <strong>Amazon EBS</strong> (Elastic Block Store). An EBS volume is essentially a virtual hard drive. Just like you cannot plug a single physical USB hard drive into three different laptops at the exact same time, AWS physically does not allow a standard EBS volume to be plugged into multiple EC2 instances (worker nodes) at once. Because of this hardware/cloud limitation, Kubernetes can only offer <strong>ReadWriteOnce (RWO)</strong> for EBS.</p>
<p>On the other hand, think about <strong>Amazon EFS</strong> (Elastic File System). EFS is a <em>network file system</em> (like a shared Google Drive or a shared folder on a Wi-Fi network). It is specifically designed to be accessed over the network by hundreds of computers simultaneously. Because the underlying cloud technology supports multiple connections, Kubernetes can offer <strong>ReadWriteMany (RWX)</strong> for EFS.</p>
<h2>Persistent Volumes (PVs) and Reclaim Policies</h2>
<p>While modern Kubernetes handles storage automatically, it is important to understand how the underlying <strong>Persistent Volume (PV)</strong> actually works.</p>
<p>Historically (and sometimes still today), setting up storage was a manual process. A cluster administrator would go into AWS, manually create a 10GB EBS volume, and then write a PV YAML file to tell Kubernetes that this disk exists.</p>
<pre><code class="language-yaml">apiVersion: v1
kind: PersistentVolume
metadata:
  name: manual-ebs-vol
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  awsElasticBlockStore:
    volumeID: vol-0abcd123456789
    fsType: ext4
  persistentVolumeReclaimPolicy: Retain
</code></pre>
<p>When your Pod creates a PVC (the claim), Kubernetes looks at all the available manual PVs and tries to find a match.</p>
<p>But what happens when your application is deleted and the PVC is destroyed? Does the data stay or get deleted? That is controlled by the <strong>Reclaim Policy</strong> inside the PV:</p>
<ul>
<li><p><strong>Retain:</strong> The PVC is deleted, but the PV and the actual physical disk (and its data) are kept. The volume is considered "released," but no other claim can use it until an administrator manually cleans it up. This is the safest option for critical databases.</p>
</li>
<li><p><strong>Delete:</strong> When the PVC is deleted, Kubernetes automatically deletes the PV <em>and</em> tells the cloud provider to delete the underlying disk (like the AWS EBS volume). The data is permanently gone.</p>
</li>
<li><p><strong>Recycle:</strong> <em>(Mostly deprecated now)</em> This keeps the PV but runs a basic scrub command (<code>rm -rf /volume/*</code>) to wipe the data so a new PVC can claim it.</p>
</li>
</ul>
<h3>The problem with manual PVs</h3>
<p>Imagine a simple use case: You are a developer deploying a new database, and your PVC asks for <strong>15GB</strong> of storage.</p>
<p>If your administrator only pre-created PVs that are <strong>10GB</strong> and <strong>50GB</strong>, your claim won't fit the 10GB one. Kubernetes might bind you to the 50GB PV instead, wasting 35GB of expensive cloud storage. Or worse, if there are no available PVs left, your Pod will be stuck in a <code>Pending</code> state while you wait for an IT ticket to be resolved so someone can manually provision a new disk.</p>
<p>This manual matching game—guessing how much storage developers will need and pre-creating disks—does not scale well in the cloud.</p>
<p>This exact bottleneck is why modern Kubernetes setups use <strong>dynamic provisioning</strong> via a <strong>StorageClass</strong>.</p>
<p><em>(...and then you transition right into your already-written StorageClass section!)</em></p>
<h2>StorageClass and dynamic provisioning</h2>
<p>In modern Kubernetes setups, you usually do not create Persistent Volumes manually. Instead, you use a <strong>StorageClass</strong> and let Kubernetes dynamically provision the storage when a PVC is created.</p>
<p>Here’s an example StorageClass for AWS EBS:</p>
<pre><code class="language-yaml">apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ebs-sc
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
  type: gp3
</code></pre>
<p>Then your PVC can reference it:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ebs-claim
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: ebs-sc
  resources:
    requests:
      storage: 10Gi
</code></pre>
<p>When this PVC is used by a Pod, Kubernetes works with the storage backend to create the actual volume.</p>
<p>This is called <strong>dynamic provisioning</strong>. It is much more practical than pre-creating every disk manually.</p>
<h3>EBS, EFS, and S3</h3>
<p>If you are deploying on AWS, the common storage options usually map like this:</p>
<p><strong>EBS</strong><br />Best for block storage attached to one node at a time. Good for databases or workloads where a Pod needs a dedicated disk. Usually works with <code>ReadWriteOnce</code>.</p>
<p><strong>EFS</strong><br />Best for shared file storage across multiple nodes and Pods. Useful when many Pods need to read and write the same files. Often used with <code>ReadWriteMany</code>.</p>
<p><strong>S3</strong><br />This is object storage, not a normal filesystem volume in the same way as EBS or EFS. In most application setups, you do not mount S3 like a regular disk. Instead, the app talks to S3 using an SDK or API. For things like user uploads, backups, images, or documents, this is often the better design.</p>
<p>A simple way to decide is:</p>
<ul>
<li><p>Need one disk for one workload? Use <strong>EBS</strong>.</p>
</li>
<li><p>Need shared file access across Pods? Use <strong>EFS</strong>.</p>
</li>
<li><p>Need object storage for files and assets? Use <strong>S3</strong>.</p>
</li>
</ul>
<p>For most application developers, just understanding this difference already saves a lot of confusion.</p>
<h2>Config and secrets</h2>
<h3>ConfigMaps</h3>
<p>Applications usually need configuration: environment names, feature flags, URLs, ports, or non-sensitive settings.</p>
<p>You could hardcode those values in the image, but that makes deployments rigid and hard to change. A better approach is to keep the configuration outside the image.</p>
<p>That is where ConfigMaps come in.</p>
<p>Example:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  NODE_ENV: "production"
  APP_NAME: "my-service"
</code></pre>
<p>You can consume this in a Pod as environment variables:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: config-demo
spec:
  containers:
    - name: app
      image: nginx
      envFrom:
        - configMapRef:
            name: app-config
</code></pre>
<h3>Secrets (and why you shouldn't write them in YAML)</h3>
<p>Secrets are used for sensitive data like database passwords, API keys, and private credentials.</p>
<p>Normally, a basic Kubernetes Secret looks like this:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Secret
metadata:
  name: app-secret
type: Opaque
stringData:
  DB_PASSWORD: myStrongPassword
</code></pre>
<p>This is better than hardcoding passwords directly in your application code, but it introduces a massive problem for real-world projects: <strong>How do you store this YAML file?</strong></p>
<p>If you commit this to Git, your password is out in the open. Even if you use Kubernetes' default base64 encoding instead of <code>stringData</code>, base64 is just encoding, <em>not encryption</em>. Anyone who can read your repository can decode your passwords in seconds.</p>
<h3>The Best Practice: External Secrets Operator</h3>
<p>In modern, real-world systems, the standard best practice is to never store secrets in Kubernetes YAML at all. Instead, you store them in a secure vault (like <strong>AWS Secrets Manager</strong>, HashiCorp Vault, or Azure Key Vault) and use a tool called the <strong>External Secrets Operator (ESO)</strong> to bridge the gap.</p>
<p>Here is how the flow actually works:</p>
<ol>
<li><p>You put your real secrets inside AWS Secrets Manager.</p>
</li>
<li><p>You write an <code>ExternalSecret</code> YAML file that simply <em>points</em> to AWS.</p>
</li>
<li><p>The operator securely fetches the values from AWS and dynamically creates a native Kubernetes Secret inside the cluster for you.</p>
</li>
</ol>
<p>Here is what your <code>ExternalSecret</code> YAML looks like:</p>
<pre><code class="language-yaml">apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-external-secret
spec:
  refreshInterval: "1h"
  secretStoreRef:
    name: aws-secrets-manager # Points to your AWS config
    kind: ClusterSecretStore
  target:
    name: app-secret-generated # The name of the Kubernetes Secret it will create!
  dataFrom:
    - extract:
        key: production/database/credentials # The exact path in AWS Secrets Manager
</code></pre>
<p>Notice that there are no actual passwords in this file. It is completely safe to commit this to your Git repository.</p>
<h3>Consuming the Secret in your Pod</h3>
<p>Once the External Secrets Operator does its magic, it creates a normal Kubernetes Secret named <code>app-secret-generated</code> in your cluster.</p>
<p>Now, just like we did with ConfigMaps, the cleanest way to inject this into your Pod is to reference the whole thing at once using <code>envFrom</code> and <code>secretRef</code>:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: secret-demo
spec:
  containers:
    - name: app
      image: nginx
      envFrom:
        - secretRef:
            name: app-secret-generated # Pulling in the auto-created secret
</code></pre>
<p>By combining <strong>External Secrets</strong> with the <code>envFrom</code> pattern, you achieve the ultimate setup:</p>
<ul>
<li><p>Your Git repositories are completely free of hardcoded passwords.</p>
</li>
<li><p>You can centrally manage and rotate secrets in AWS.</p>
</li>
<li><p>Your Kubernetes Deployment YAML stays incredibly short, clean, and secure.</p>
</li>
</ul>
<hr />
<p>With this, we move from just understanding Kubernetes to actually working with it.</p>
<p>In this part, we started with YAML, looked at how resources are declared, created a Pod, understood why ReplicaSets and Deployments exist, touched on DaemonSets, and then went deeper into storage, configuration, and secrets. These are the things that make Kubernetes feel practical rather than abstract.</p>
<p>In the next part of this series, we’ll move into another major piece of the puzzle: how applications running inside the cluster actually communicate with each other and how traffic reaches them from the outside world.</p>
<p>Hope you found this useful.<br />Reach out to me on <a href="https://x.com/SagnikGuru">Twitter</a> and/or <a href="https://www.linkedin.com/in/sagnik-guru-46790919a">LinkedIn</a>.</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[Kubernetes Fundamentals Part 1: Why Kubernetes and How It Works]]></title><description><![CDATA[Kubernetes(K8's) has been on my learning list for years. I’ve tried picking it up multiple times during my three years as a software developer, but honestly, the terminology and architecture always th]]></description><link>https://kubernetes-sagnik.hashnode.dev/kubernetes-fundamentals-part-1-why-kubernetes-and-how-it-works</link><guid isPermaLink="true">https://kubernetes-sagnik.hashnode.dev/kubernetes-fundamentals-part-1-why-kubernetes-and-how-it-works</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Sagnik Guru]]></dc:creator><pubDate>Sun, 22 Mar 2026 08:55:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6802f5e18e6fd72ba8c17773/50dc8fa7-8ce9-467a-b0b4-6bc3ba1d099d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Kubernetes(K8's) has been on my learning list for years. I’ve tried picking it up multiple times during my three years as a software developer, but honestly, the terminology and architecture always threw me off.</p>
<p>Recently, though, my project demanded it. So this time, I couldn’t back out — I had to get it working. While figuring things out (with a mix of trial, docs, and a bit of help from AI), I finally started to see how it all fits together. This blog series is me sharing that journey. It’s not a full Kubernetes course, but a simple and practical guide that focuses on the fundamentals — enough for any developer to feel comfortable working on projects deployed with Kubernetes.</p>
<h1>Why and What?</h1>
<img src="https://cdn.hashnode.com/uploads/covers/6802f5e18e6fd72ba8c17773/2b878126-a9c3-450f-b1c0-c73d65e10830.png" alt="" style="display:block;margin:0 auto" />

<p>Imagine an application made up of multiple Docker containers running across multiple servers. In such a setup, you need capabilities like monitoring containers, load balancing traffic between them, automatically restarting failed containers, scaling based on CPU or memory usage, and performing deployments with zero downtime.</p>
<p>While all of this can technically be achieved using manual scripts and cloud provider services, these are common challenges in distributed systems. Kubernetes was introduced to solve these problems in a standardized way, allowing developers to declare the desired state of the system, while Kubernetes handles how to achieve and maintain that state.</p>
<h1>Why Containers?</h1>
<p>Kubernetes uses containers because they package the application along with all its dependencies, so the same application can run reliably across different machines without environment issues. Containers also start quickly, which makes scaling and recovery faster when traffic increases or failures happen. Multiple containers can run on the same machine without needing separate operating systems, which reduces resource usage and cost.</p>
<p>If Kubernetes were to use raw machines or full virtual machines, each deployment would require setting up and maintaining separate environments, startup times would be slower, scaling would take longer, and overall infrastructure management would become more complex and expensive.</p>
<h1>Why Not?</h1>
<p>Kubernetes helps applications handle scaling and failures automatically. But it is harder to use because your system is split into many components, like clusters, nodes, and services that all need to work together. It can increase engineering costs because it requires people who understand how to design and manage this system properly.</p>
<p>It also adds extra operational effort because instead of managing a single application on a server, you are managing a whole platform where many applications run together and depend on each other. This makes the system more complex to reason about and operate, especially for small teams that do not need large-scale infrastructure.</p>
<h1>Architecture</h1>
<img src="https://cdn.hashnode.com/uploads/covers/6802f5e18e6fd72ba8c17773/892d473e-d359-4627-8904-cc70b347c477.png" alt="" style="display:block;margin:0 auto" />

<p>In order to understand the architecture of k8's, we need to first learn about a few building blocks of k8's shown in the image</p>
<h2>Components of k8's</h2>
<h3>Pod</h3>
<p>A <strong>Pod in Kubernetes</strong> is the smallest unit of deployment. It can contain one or more containers that run together on the same node. A Pod acts as a wrapper around these containers and provides a shared environment/resources for them. Kubernetes uses Pods as the basic unit to schedule, monitor, restart, and manage application workloads in a consistent way.</p>
<h3>Node</h3>
<p>A node is nothing but a server, a physical server, or a virtual machine, like an EC2. In Kubernetes, there are two types of nodes</p>
<ul>
<li><p>Control Plane / Master Node</p>
</li>
<li><p>Worker Node</p>
</li>
</ul>
<h3>Control Plane / Master Node</h3>
<p>They are special nodes of k8s that are responsible for managing the worker nodes, storing information regarding pods and nodes, taking requests from clients, and performing them</p>
<h3>Worker Nodes</h3>
<p>They are the nodes running your application containers inside pods</p>
<h3>Cluster</h3>
<p>The complete system of control plane nodes and worker nodes is called a k8s cluster.</p>
<h3>kubectl</h3>
<p>This is the command-line tool (the “Kubernetes client”) you use to interact with your cluster. Whenever you run commands like creating a pod or checking the status of a deployment, kubectl sends those requests to the Kubernetes system.</p>
<h3>API Server</h3>
<p>The API Server is like the brain’s messenger. It receives your requests from kubectl, processes them, and then passes the right instructions to different parts of the cluster. It also talks to other control plane components to keep everything in sync.</p>
<h3>Scheduler</h3>
<p>When you ask Kubernetes to create a Pod, the Scheduler decides which worker node the Pod should run on. It looks at available resources on each node and picks the one that fits best.</p>
<h3>Controller Manager</h3>
<p>Think of this as the system’s caretaker. The Controller Manager constantly watches what’s happening in the cluster. If something isn’t how it’s supposed to be — like a crashed Pod or a missing node — it takes action to fix it and bring the system back to the desired state.</p>
<h3>etcd</h3>
<p>etcd is a small but crucial part — it’s a key-value database that stores all the cluster’s information, such as configuration data, the state of Pods, and node details. The API Server reads and writes data here to keep track of what the cluster should look like.</p>
<h3>Kubelet</h3>
<p>Each worker node has a small agent called Kubelet running inside it. It talks to the API Server and makes sure that the containers (Pods) assigned to that node are actually running and healthy.</p>
<h3>Kube Proxy</h3>
<p>The Kube Proxy handles networking inside each worker node. It makes sure Pods can talk to each other — even across different nodes — and that traffic flows correctly between services and Pods.</p>
<h2>Big Picture</h2>
<p>Let's understand the flow of how these components combine and work when a simple command like <code>kubectl run my-pod --image=nginx</code></p>
<ol>
<li><p><strong>kubectl → API Server</strong></p>
<ul>
<li><p><code>kubectl</code> sends a request to the API Server saying “create a Pod called <code>my-pod</code> with this image.”</p>
</li>
<li><p>The API Server checks if the request is valid and if you have permission to do it.</p>
</li>
</ul>
</li>
<li><p><strong>API Server → etcd (store desired state)</strong></p>
<ul>
<li>The API Server writes a new Pod object into etcd, which now says: “There should be a Pod named <code>my-pod</code> in the cluster.”</li>
</ul>
</li>
<li><p><strong>Scheduler picks a worker node</strong></p>
<ul>
<li><p>The Scheduler watches the API Server for Pods that do not have a node assigned yet.</p>
</li>
<li><p>It sees <code>my-pod</code>, looks at all worker nodes and their resources, and decides: “Run <code>my-pod</code> on worker node A.”</p>
</li>
<li><p>It updates that decision through the API Server, which is again stored in etcd.</p>
</li>
</ul>
</li>
<li><p><strong>API Server → kubelet on chosen node</strong></p>
<ul>
<li><p>The kubelet on worker node A regularly talks to the API Server and asks, “What Pods should I be running?”</p>
</li>
<li><p>It sees that <code>my-pod</code> is now assigned to its node.</p>
</li>
</ul>
</li>
<li><p><strong>kubelet creates the container</strong></p>
<ul>
<li><p>The kubelet pulls the <code>nginx</code> image (if not already present) and starts the container inside a Pod on that worker node.</p>
</li>
<li><p>It then reports status back to the API Server. “<code>my-pod</code> is running.”</p>
</li>
</ul>
</li>
<li><p><strong>API Server updates etcd (current state)</strong></p>
<ul>
<li><p>The API Server writes the current status of <code>my-pod</code> (Pending → Running) into etcd.</p>
</li>
<li><p>When you run <code>kubectl get pods</code>, kubectl again talks to the API Server, which reads from etcd and returns the Pod status.</p>
</li>
</ul>
</li>
</ol>
<hr />
<p>With this, we come to the end of this first Kubernetes fundamentals article, where we set the stage with the “why,” the core concepts, and a clear view of how the cluster is put together.</p>
<p>In the next part of this series, we’ll move from understanding the system to <strong>managing real applications on Kubernetes</strong>: how workloads are defined, rolled out, updated, and kept running reliably in production.</p>
<p>Hope you found this useful!<br />Reach out to me on <a href="https://x.com/SagnikGuru">Twitter</a> and/or <a href="https://www.linkedin.com/in/sagnik-guru-46790919a">LinkedIn</a>.</p>
<hr />
]]></content:encoded></item></channel></rss>