<?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: softverseAdmin</title>
    <description>The latest articles on DEV Community by softverseAdmin (@softverseadmin).</description>
    <link>https://dev.to/softverseadmin</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%2F4217550%2Fc892b60b-191f-41b6-9516-5fd2465af4a2.png</url>
      <title>DEV Community: softverseAdmin</title>
      <link>https://dev.to/softverseadmin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/softverseadmin"/>
    <language>en</language>
    <item>
      <title>Amazon EKS in 2026: Managed Kubernetes on AWS, Explained for Working Engineers</title>
      <dc:creator>softverseAdmin</dc:creator>
      <pubDate>Sat, 31 Oct 2026 09:15:41 +0000</pubDate>
      <link>https://dev.to/softverseadmin/amazon-eks-in-2026-managed-kubernetes-on-aws-explained-for-working-engineers-4749</link>
      <guid>https://dev.to/softverseadmin/amazon-eks-in-2026-managed-kubernetes-on-aws-explained-for-working-engineers-4749</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; EKS gives you a managed Kubernetes control plane and leaves everything else — nodes, networking shape, IAM wiring, add-ons — as your design decisions. The choices that matter: nodes come as managed node groups (the sensible default), self-managed groups (when you need control), or Fargate (serverless pods, with real constraints); Karpenter increasingly replaces static groups for teams that want nodes to follow demand. Networking runs on your VPC — pods get real VPC IPs from your subnets, which makes the &lt;a href="https://www.devopsenginer.com/blog/aws-vpc-guide-2026" rel="noopener noreferrer"&gt;VPC design&lt;/a&gt; a Kubernetes decision. IAM meets Kubernetes through Pod Identity (the current, simpler answer to "how does this pod get AWS permissions"), and the cluster is only production-shaped once the add-ons land: EBS CSI for storage, the Load Balancer Controller for ingress, external-dns and cert-manager for the public face. What EKS removes is the control plane burden. Everything in the rest of this series still applies — it just runs on AWS now.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.devopsenginer.com/blog/amazon-eks-guide-2026" rel="noopener noreferrer"&gt;DevOps Enginer&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most teams choosing EKS are not really choosing Kubernetes — they already know it. They are choosing to stop operating a control plane. What surprises them is how much remains theirs: EKS is managed Kubernetes, not managed &lt;em&gt;everything&lt;/em&gt;, and the gap between "cluster created" and "cluster production-ready" is a series of decisions this guide walks through.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AWS manages, and what it pointedly does not
&lt;/h2&gt;

&lt;p&gt;AWS runs the control plane — API servers and etcd across multiple AZs, patched and highly available — and charges a flat per-cluster hourly fee for it. That is the entire managed surface. &lt;strong&gt;Nodes are your EC2 instances&lt;/strong&gt; (or Fargate tasks), the VPC is yours, IAM is yours, and the add-ons that make a cluster useful are yours to install and maintain. Understanding this boundary prevents the most common EKS disappointment: expecting an app platform and receiving excellent infrastructure primitives.&lt;/p&gt;

&lt;p&gt;Cluster creation itself (eksctl, Terraform, console) is the easy afternoon. The decisions below are the ones with consequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nodes: three ways to run pods, and a fourth that changes the question
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Managed node groups&lt;/strong&gt; — AWS handles the EC2 lifecycle (provisioning, AMI updates, draining on termination) behind a group definition: instance type, size range, subnets, labels. This is the default recommendation: boring, supported, and sufficient for most clusters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-managed node groups&lt;/strong&gt; — you own the Auto Scaling group and the bootstrap. Justified by custom AMIs, unusual instance configuration, or requirements the managed path doesn't expose. You also own every patch and every upgrade chore — the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-upgrades-guide-2026" rel="noopener noreferrer"&gt;upgrades guide&lt;/a&gt; applies with extra steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fargate&lt;/strong&gt; — no nodes at all: each pod runs in an isolated micro-VM with a per-pod price. The constraints are the fine print: no DaemonSets, no persistent volumes beyond EFS, pod sizes from a fixed menu, and startup latency that batch and CI workloads forgive and latency-sensitive services don't. Excellent for spiky, stateless, low-count workloads; a poor default for a whole cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Karpenter&lt;/strong&gt; — the open-source autoscaler from AWS that increasingly replaces static groups and the cluster-autoscaler pattern: it watches pending pods and launches &lt;em&gt;right-sized&lt;/em&gt; instances (type, family, AZ chosen per workload) in under a minute, then consolidates under-used nodes away. For clusters whose demand moves — most clusters — Karpenter is where the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-autoscaling-guide-2026" rel="noopener noreferrer"&gt;autoscaling story&lt;/a&gt; lands on AWS, and its cost effect is the rare one that shows up on the bill uninvited, in the good direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Networking: your VPC is the cluster network
&lt;/h2&gt;

&lt;p&gt;EKS uses the AWS VPC CNI by default, which means &lt;strong&gt;pods receive real IP addresses from your VPC subnets&lt;/strong&gt;. The consequences are pleasant (pods are first-class network citizens; security groups can even attach per-pod) and constraining (pod density per node is limited by ENI/IP capacity, and subnet IP space is a real planning input — small subnets plus big clusters is a known way to run out of addresses). Prefix delegation raises pod density substantially where needed. This is why the subnet sizing in the VPC guide leaned generous: Kubernetes spends IP addresses like it means it.&lt;/p&gt;

&lt;p&gt;The public face of the cluster is the &lt;strong&gt;AWS Load Balancer Controller&lt;/strong&gt;: annotate an Ingress or Service and it provisions an ALB/NLB with target groups wired to your pods — the AWS-native version of the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-ingress-networking-basics-2026" rel="noopener noreferrer"&gt;ingress patterns&lt;/a&gt; covered earlier. Add &lt;strong&gt;external-dns&lt;/strong&gt; (Route 53 records follow your ingresses automatically) and &lt;strong&gt;cert-manager&lt;/strong&gt; with ACM or Let's Encrypt, and "deploy an app, get a public HTTPS URL" becomes the paved road rather than a manual ritual.&lt;/p&gt;

&lt;h2&gt;
  
  
  IAM inside the cluster: Pod Identity is the current answer
&lt;/h2&gt;

&lt;p&gt;Pods need AWS permissions — read this bucket, publish to that queue — and the cardinal rule is unchanged from everywhere else in AWS: &lt;strong&gt;per-workload roles, never node-wide credentials.&lt;/strong&gt; A node role big enough for every pod on the node is a privilege-escalation welcome mat, per the &lt;a href="https://www.devopsenginer.com/blog/aws-iam-least-privilege-guide-2026" rel="noopener noreferrer"&gt;IAM least-privilege guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;EKS has had two mechanisms. &lt;strong&gt;IRSA&lt;/strong&gt; (IAM Roles for Service Accounts) works via OIDC and annotations and remains everywhere in existing clusters. &lt;strong&gt;EKS Pod Identity&lt;/strong&gt; is the newer, simpler successor: associate an IAM role with a service account through the EKS API, an agent on the node handles credentials, and the annotation archaeology of IRSA goes away. New clusters should start on Pod Identity; existing ones can run both while migrating. Either way, the &lt;em&gt;design&lt;/em&gt; is identical: one role per workload, scoped to the resources that workload touches, with the cluster components (CSI driver, LB controller, Karpenter, external-dns) each holding their own deliberately-sized roles.&lt;/p&gt;

&lt;h2&gt;
  
  
  The add-ons that make it production-shaped
&lt;/h2&gt;

&lt;p&gt;A fresh EKS cluster is deliberately minimal. The standard load-out, whether installed as EKS add-ons or via Helm/Argo CD:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EBS CSI driver&lt;/strong&gt; — without it, PersistentVolumes on EBS don't exist; the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-storage-guide-2026" rel="noopener noreferrer"&gt;storage model&lt;/a&gt; (gp3 classes, Retain policies, snapshots) hangs off this driver on AWS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Load Balancer Controller&lt;/strong&gt; — ingress and services, as above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CoreDNS and kube-proxy&lt;/strong&gt; — shipped as EKS add-ons; keep their versions in step with cluster upgrades rather than discovering skew during one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metrics and logs&lt;/strong&gt; — metrics-server or the Prometheus stack for the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-observability-guide-2026" rel="noopener noreferrer"&gt;observability baseline&lt;/a&gt;, plus a log route (Fluent Bit to CloudWatch Logs or your aggregator). A cluster nobody can see into is a cluster on borrowed time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster access&lt;/strong&gt; — modern EKS uses &lt;em&gt;access entries&lt;/em&gt; (API-based authentication mapping IAM principals to Kubernetes permissions), replacing the hand-edited &lt;code&gt;aws-auth&lt;/code&gt; ConfigMap that caused a generation of "I locked everyone out of the cluster" incidents. Whoever provisions the cluster should set access policy deliberately on day one; the creator-admin trap (only the creating identity can administer, ever) is an old rite of passage best skipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything after installation is the standard Kubernetes operating model — &lt;a href="https://www.devopsenginer.com/blog/argo-cd-tutorial-gitops-2026" rel="noopener noreferrer"&gt;GitOps delivery&lt;/a&gt;, &lt;a href="https://www.devopsenginer.com/blog/kubernetes-deployment-strategies-2025-complete-guide" rel="noopener noreferrer"&gt;progressive rollouts&lt;/a&gt;, &lt;a href="https://www.devopsenginer.com/blog/kubernetes-security-basics-guide-2026" rel="noopener noreferrer"&gt;security basics&lt;/a&gt;. EKS changes the substrate, not the discipline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrades and cost: the two recurring appointments
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Upgrades&lt;/strong&gt; follow the standard Kubernetes rhythm with AWS holding the control-plane end: you trigger the control-plane upgrade one minor version at a time, then roll node groups (or let Karpenter drift-replace nodes onto the new AMI), then bump add-ons. Support windows matter here: standard support for a Kubernetes version on EKS runs about 14 months from its EKS release, with paid extended support beyond — the calendar in the upgrades guide applies verbatim.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost&lt;/strong&gt; has three components people chronically under-model: the per-cluster control-plane fee (multiplied by every forgotten dev cluster — see the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-cost-optimization-guide-2026" rel="noopener noreferrer"&gt;Kubernetes cost guide&lt;/a&gt; for why cluster sprawl is the quiet line item), the nodes themselves (where Karpenter's right-sizing and consolidation do real work, plus Spot for fault-tolerant capacity), and the AWS perimeter — NAT gateways, load balancers and data transfer that the VPC design decided before the cluster existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expecting a platform.&lt;/strong&gt; EKS is infrastructure with a managed control plane. Teams that budget zero platform work discover the add-on, IAM and networking decisions arriving anyway, unscheduled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The node role that does everything.&lt;/strong&gt; One broad role for the node group means every pod inherits it. Per-workload identity via Pod Identity is the entire fix, and it's cheap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subnets sized for the demo.&lt;/strong&gt; Pods consume VPC IPs directly; small subnets cap cluster growth in a way that surfaces mid-incident. Size for the cluster you expect, with the VPC's spare range held back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;aws-auth archaeology.&lt;/strong&gt; Hand-editing the access ConfigMap (on clusters still using it) locks teams out with one typo. Prefer access entries; if you must edit aws-auth, do it from a session that stays open until a second login is verified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add-ons drifting behind the cluster version.&lt;/strong&gt; CSI and LB controller versions have compatibility windows; treat add-on upgrades as part of the cluster upgrade runbook, not a separate someday.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cluster per team per environment, forever.&lt;/strong&gt; The per-cluster fee plus duplicated add-ons and perimeter makes sprawl a budget line. Consolidate with namespaces and per-team IAM where isolation requirements genuinely allow it — and keep dedicated clusters where they don't (compliance, blast radius), knowingly.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;EKS vs self-managed Kubernetes on EC2?&lt;/strong&gt;&lt;br&gt;
Self-managed gives total control and total responsibility: control-plane HA, etcd backups, and patching are yours. EKS removes exactly that layer for a flat fee. Teams choose self-managed for specific control or cost-at-scale reasons; everyone else is buying back their weekends with EKS, correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EKS vs ECS Fargate?&lt;/strong&gt;&lt;br&gt;
If the team knows Kubernetes or needs its ecosystem (Helm charts, operators, the tooling in this series), EKS. If the need is "run these containers on AWS with minimal concepts," ECS is simpler and entirely respectable. The honest test is which ecosystem your hiring, tooling and existing workloads already speak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run a cluster with no managed node groups at all?&lt;/strong&gt;&lt;br&gt;
Yes — Fargate-only clusters exist and suit small, spiky, stateless estates. The DaemonSet and storage constraints decide for most teams; mixed setups (node groups for the baseline, Fargate for burst jobs) are common.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many clusters should we have?&lt;/strong&gt;&lt;br&gt;
Fewer than instinct suggests, more than one for anything serious: separate production from non-production at minimum. Beyond that, let blast radius, compliance boundaries and team autonomy argue it out — with the per-cluster and perimeter costs on the table while they do.&lt;/p&gt;

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

&lt;p&gt;EKS is a trade stated plainly: AWS operates the control plane; you keep the architecture. Choose node strategy deliberately (managed groups by default, Karpenter when demand moves), design the VPC like the cluster depends on it — because it literally runs on it — wire IAM per workload with Pod Identity, and install the add-ons that turn a cluster into a platform. Do those things and EKS delivers what it promises: Kubernetes without the control-plane pager. The rest of running Kubernetes well is, as ever, on you — and it's what the other fifteen parts of this series are for.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The free &lt;a href="https://www.devopsenginer.com" rel="noopener noreferrer"&gt;Kubernetes Production Checklist&lt;/a&gt; (via the newsletter on the homepage) includes the production-readiness items this guide builds toward — access, backups, observability and upgrade readiness.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>aws</category>
      <category>eks</category>
      <category>devops</category>
    </item>
    <item>
      <title>AWS VPC in 2026: The DevOps Guide to Subnets, Routing and Not Locking Yourself Out</title>
      <dc:creator>softverseAdmin</dc:creator>
      <pubDate>Sat, 31 Oct 2026 08:56:52 +0000</pubDate>
      <link>https://dev.to/softverseadmin/aws-vpc-in-2026-the-devops-guide-to-subnets-routing-and-not-locking-yourself-out-2gd</link>
      <guid>https://dev.to/softverseadmin/aws-vpc-in-2026-the-devops-guide-to-subnets-routing-and-not-locking-yourself-out-2gd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; A VPC is your own slice of AWS networking, and almost everything about it is decided by five choices: the CIDR block (bigger than you think you need, because it can never grow), the subnet layout (public for load balancers, private for workloads, isolated for data — spread across at least two, ideally three AZs), the route tables (a subnet is only "public" because its route table points at the internet gateway), NAT gateways (how private subnets reach the internet — one per AZ, because a shared NAT is both a bottleneck and a bill), and the two security layers (security groups, stateful and attached to instances; network ACLs, stateless and attached to subnets). Add VPC endpoints for AWS services before the NAT bill teaches you why, enable flow logs from day one, and never use the default VPC for anything you care about.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.devopsenginer.com/blog/aws-vpc-guide-2026" rel="noopener noreferrer"&gt;DevOps Enginer&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Networking is the part of AWS everyone clicks through on the way to launching something — and the part that is nearly impossible to change later. A CIDR block cannot be resized. A subnet layout chosen in an afternoon shapes every architecture decision after it. This guide is the VPC as DevOps engineers actually need to understand it: the design decisions, in the order they become permanent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CIDR block: the one-way door
&lt;/h2&gt;

&lt;p&gt;Your VPC gets an IPv4 range like &lt;code&gt;10.20.0.0/16&lt;/code&gt; — 65,536 addresses. Choose it like it matters, because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It cannot be enlarged later.&lt;/strong&gt; You can attach secondary CIDRs, but the clean answer is to start generous: a &lt;code&gt;/16&lt;/code&gt; per VPC is the common, sane default. Address space is free; running out is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It must not collide with anything it will ever talk to&lt;/strong&gt; — your other VPCs (peering and Transit Gateway demand non-overlapping ranges), your office network, your VPN ranges. Collisions discovered at integration time are miserable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick from private ranges everyone recognises&lt;/strong&gt; (&lt;code&gt;10.x&lt;/code&gt; is conventional) and keep a simple allocation register: which block belongs to which account/environment/region. A spreadsheet beats a memory, and it will be consulted at the worst possible moment if it doesn't exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Subnets carve the block per AZ and tier: a workable pattern is &lt;code&gt;/20&lt;/code&gt;s per subnet (4,096 addresses each) — public, private and data tiers across three AZs consumes nine subnets and still leaves most of a &lt;code&gt;/16&lt;/code&gt; unallocated for the future. Remember AWS reserves five addresses in every subnet (network, router, DNS, future use, broadcast), so a subnet's usable count is its size minus five.&lt;/p&gt;

&lt;h2&gt;
  
  
  Subnets: three tiers, and what "public" actually means
&lt;/h2&gt;

&lt;p&gt;The tier names describe &lt;em&gt;routing&lt;/em&gt;, not intention:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Public subnets&lt;/strong&gt; — route table has a route to an &lt;strong&gt;Internet Gateway (IGW)&lt;/strong&gt;. For: load balancers, NAT gateways, bastions — the small set of things that face the internet. Nothing else earns a public IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private subnets&lt;/strong&gt; — no IGW route; outbound internet goes via a NAT gateway in a public subnet. For: application servers, containers (your EKS/EC2 workloads), everything that should be reachable only through a load balancer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolated (data) subnets&lt;/strong&gt; — no internet route at all, in or out. For: databases, caches, queues — resources that should only ever talk to the application tier inside the VPC.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sentence worth internalising: &lt;strong&gt;a subnet is public or private solely because of its route table.&lt;/strong&gt; There is no checkbox. Misunderstanding this is the root of most "why can/can't my instance reach the internet" mysteries — the answer is always in the route table, never in the subnet's name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VPC 10.20.0.0/16
├── AZ a: public 10.20.0.0/20 · private 10.20.16.0/20 · data 10.20.32.0/20
├── AZ b: public 10.20.48.0/20 · private 10.20.64.0/20 · data 10.20.80.0/20
└── AZ c: public 10.20.96.0/20 · private 10.20.112.0/20 · data 10.20.128.0/20
    (remainder of the /16 left unallocated, on purpose)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Routing and NAT: where the money and the outages live
&lt;/h2&gt;

&lt;p&gt;Route tables attach per subnet (with one main table as the default). The public table points &lt;code&gt;0.0.0.0/0&lt;/code&gt; at the IGW; private tables point &lt;code&gt;0.0.0.0/0&lt;/code&gt; at a NAT gateway; data tables have only the local route. Two operational truths follow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NAT gateways are per-AZ infrastructure, not a shared utility.&lt;/strong&gt; One NAT gateway costs a fixed hourly fee plus per-GB processing — and routing three AZs through one NAT in AZ-a means (a) an AZ-a failure takes outbound internet from the whole VPC, and (b) you pay cross-AZ data transfer for the privilege. &lt;strong&gt;One NAT per AZ&lt;/strong&gt; is the production pattern; the &lt;a href="https://www.devopsenginer.com/blog/aws-cost-optimization-15-tactics-saved-50k-month" rel="noopener noreferrer"&gt;cost guide&lt;/a&gt; explains the bill either way. Dev environments can share one NAT and accept the risk knowingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VPC endpoints are the other half of the traffic bill.&lt;/strong&gt; Without them, your private subnets reach S3, DynamoDB and every other AWS API &lt;em&gt;through the NAT gateway&lt;/em&gt;, paying per-GB for traffic that never needed to leave AWS's network. Gateway endpoints for S3 and DynamoDB are &lt;strong&gt;free&lt;/strong&gt; — there is no excuse for their absence. Interface endpoints (PrivateLink) for the likes of ECR, CloudWatch Logs and Secrets Manager cost an hourly fee per AZ but keep agent and image-pull traffic fully private — on a busy EKS cluster, pulling images through NAT is a cost and failure-domain decision, not a default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two security layers (they are not interchangeable)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Security groups&lt;/strong&gt; attach to instances/ENIs, are &lt;strong&gt;stateful&lt;/strong&gt; (reply traffic is automatically allowed), support allow-rules only, and can reference &lt;em&gt;other security groups&lt;/em&gt; — "the database accepts 5432 from the app tier's group" is the single most useful pattern in AWS networking, because it tracks membership automatically as instances come and go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network ACLs&lt;/strong&gt; attach to subnets, are &lt;strong&gt;stateless&lt;/strong&gt; (return traffic needs its own rule — the ephemeral-port gotcha), support allow &lt;em&gt;and&lt;/em&gt; deny rules evaluated in numbered order, and apply to everything in the subnet whether you remembered it or not.&lt;/p&gt;

&lt;p&gt;The working model: security groups are the primary control, designed per-tier with group-to-group references; NACLs stay near-default (allow all) unless you need a subnet-level guardrail — a deny rule that stands even if someone edits a security group badly. Teams that build elaborate NACL logic usually discover the statelessness tax during their first debugging session at 2 a.m. And least privilege applies to who can edit these as much as to the rules themselves — network changes belong behind the same &lt;a href="https://www.devopsenginer.com/blog/aws-iam-least-privilege-guide-2026" rel="noopener noreferrer"&gt;IAM discipline&lt;/a&gt; as everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flow logs: turn them on before you need them
&lt;/h2&gt;

&lt;p&gt;VPC Flow Logs record accepted/rejected traffic metadata to CloudWatch Logs or S3. They answer the questions every network incident asks — "is traffic even reaching this subnet?" "what is talking to the database?" — and they cannot be back-filled. Enabling them at VPC creation costs almost nothing and is the difference between evidence and vibes during debugging; the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-observability-guide-2026" rel="noopener noreferrer"&gt;observability habits&lt;/a&gt; from the Kubernetes side apply here unchanged. While you're in the console: DNS resolution and DNS hostnames &lt;strong&gt;on&lt;/strong&gt; (private DNS names make internal life bearable), and no workloads in the &lt;strong&gt;default VPC&lt;/strong&gt; — it exists for experiments, its layout is nobody's design, and its flat structure has surprised many auditors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting beyond one VPC
&lt;/h2&gt;

&lt;p&gt;Sooner or later the VPC needs neighbours, and each option has its shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VPC peering&lt;/strong&gt; — a direct link between two VPCs: cheap, fast, and &lt;em&gt;non-transitive&lt;/em&gt; (A↔B and B↔C does not give A↔C), with CIDRs that must not overlap. Fine for a pair; a mesh of many VPCs becomes unmanageable surprisingly fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transit Gateway&lt;/strong&gt; — the hub: VPCs and VPNs attach to one router, routes propagate sensibly, and the architecture stays legible at ten VPCs and beyond. It costs more than peering and earns it the moment "many" enters the sentence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPN / Direct Connect&lt;/strong&gt; — to your office or data centre. Start with site-to-site VPN (fast to stand up, internet-bound); Direct Connect is the committed, consistent-bandwidth option with lead times measured in weeks, ordered when the VPN's variability becomes a real complaint rather than a theoretical one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For Kubernetes specifically:&lt;/strong&gt; EKS nodes live in your private subnets and inherit all of this — the cluster's outbound internet, endpoint choices and NAT layout are VPC decisions made before the first node joins, per the networking thinking in the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-ingress-networking-basics-2026" rel="noopener noreferrer"&gt;Kubernetes networking guide&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And define it all as code. A VPC clicked together in the console is a snowflake nobody can rebuild; the same network as a &lt;a href="https://www.devopsenginer.com/blog/terraform-modules-guide-2026" rel="noopener noreferrer"&gt;Terraform module&lt;/a&gt; — CIDR, tiers and AZ count as variables — is an asset every new environment inherits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The too-small CIDR.&lt;/strong&gt; A &lt;code&gt;/24&lt;/code&gt; VPC feels tidy until the second AZ or the EKS cluster arrives (Kubernetes networking is address-hungry). You cannot grow it; you can only rebuild. Start at &lt;code&gt;/16&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overlapping ranges across VPCs or with the office.&lt;/strong&gt; Peering, Transit Gateway and VPN all refuse to cooperate with collisions. Allocate from a register, not from memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workloads in public subnets "temporarily".&lt;/strong&gt; Temporary is forever. If it doesn't face the internet for a living, it's private.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One NAT gateway for three AZs&lt;/strong&gt; — a single point of failure plus cross-AZ transfer charges. One per AZ in production; shared only where the risk is understood and accepted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No S3/DynamoDB gateway endpoints.&lt;/strong&gt; Free to add, and their absence sends AWS's own traffic through the metered NAT. The most expensive checkbox in AWS is the one that costs nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elaborate NACL rule sets.&lt;/strong&gt; Stateless rules with ephemeral ports turn routine debugging into archaeology. Security groups do the real work; NACLs are guardrails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flow logs "later".&lt;/strong&gt; Later is during the incident, when the missing history would have answered the question. On at creation, always.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How many subnets do I actually need?&lt;/strong&gt;&lt;br&gt;
Per AZ: one public, one private, one data — so 6 for a solid 2-AZ setup, 9 for 3 AZs. More tiers (a separate management tier, say) are legitimate; more &lt;em&gt;AZs than your region offers in your account&lt;/em&gt; is the usual surprise, so check AZ availability before finalising the layout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I change the CIDR after creating the VPC?&lt;/strong&gt;&lt;br&gt;
You can add secondary CIDR blocks, but you cannot remove or shrink the primary one. Treat the primary block as permanent — that's why the sizing advice leans generous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Public IP on an instance in a private subnet — possible?&lt;/strong&gt;&lt;br&gt;
No, and that's the point: no IGW route means no public reachability, whatever the instance settings say. Reach the internet via NAT (outbound) or not at all (data tier); reach the instance via load balancer, bastion or SSM Session Manager — which, not incidentally, is the modern answer to "how do I SSH in": usually, you don't; Session Manager gives audited shell access without any inbound ports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VPC peering or Transit Gateway?&lt;/strong&gt;&lt;br&gt;
Two VPCs that need each other: peering. Anything beyond a pair, or anything involving a shared services VPC or on-prem: Transit Gateway. The cost difference is real but small next to the operational difference at scale.&lt;/p&gt;

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

&lt;p&gt;A VPC rewards the teams who treat its first afternoon as design, not setup: a generous non-overlapping CIDR, three tiers across multiple AZs, route tables understood rather than assumed, NAT per AZ, free gateway endpoints in place, security groups referencing each other, and flow logs recording from the first packet. None of it is glamorous, and almost none of it can be fixed cheaply later — which is precisely why it's worth a guide, and worth doing once, properly.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;More AWS and Kubernetes guides are published weekly on &lt;a href="https://www.devopsenginer.com" rel="noopener noreferrer"&gt;DevOps Enginer&lt;/a&gt; — the free Kubernetes Production Checklist is available via the newsletter on the homepage.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>vpc</category>
      <category>networking</category>
      <category>devops</category>
    </item>
    <item>
      <title>Terraform Modules in 2026: Reusable Infrastructure Without the Copy-Paste</title>
      <dc:creator>softverseAdmin</dc:creator>
      <pubDate>Sat, 31 Oct 2026 08:43:16 +0000</pubDate>
      <link>https://dev.to/softverseadmin/terraform-modules-in-2026-reusable-infrastructure-without-the-copy-paste-jad</link>
      <guid>https://dev.to/softverseadmin/terraform-modules-in-2026-reusable-infrastructure-without-the-copy-paste-jad</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; A Terraform module is infrastructure with an interface: variables in, outputs out, opinions inside. Good modules wrap one coherent thing (a network, a cluster, an application stack), expose only the decisions callers genuinely need to make, give sensible defaults for the rest, and publish outputs that let other modules compose on top. Version them like software — semantic versions, pinned references, a changelog — because a module edit silently changes every environment that consumes it on the next plan. The failure modes are consistent: the god-module that tries to cover every case and satisfies none, the abstraction leak that forces callers to know the internals anyway, and the unpinned &lt;code&gt;main&lt;/code&gt; branch reference that turns a routine apply into a surprise upgrade. Modules done well are how one engineer maintains ten environments; done badly, they're how a copy-paste problem becomes a shared copy-paste problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.devopsenginer.com/blog/terraform-modules-guide-2026" rel="noopener noreferrer"&gt;DevOps Enginer&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every growing Terraform codebase reaches the same moment: the staging VPC definition looks exactly like the production one except for six values, someone copies the folder, and from that day the two networks drift apart one forgotten edit at a time. Modules are Terraform's answer — and like most answers, they have their own failure modes. This guide covers the design decisions that separate a module library from a junk drawer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a module actually is
&lt;/h2&gt;

&lt;p&gt;Strip the ceremony away: any directory of &lt;code&gt;.tf&lt;/code&gt; files is a module. The root module is the one you run &lt;code&gt;terraform apply&lt;/code&gt; in; child modules are directories (or registry entries, or git repos) that the root calls with a &lt;code&gt;module&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"network"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"git::https://example.com/infra-modules.git//network?ref=v1.4.0"&lt;/span&gt;
  &lt;span class="c1"&gt;# or: source = "./modules/network"   (local)&lt;/span&gt;
  &lt;span class="c1"&gt;# or: source = "terraform-aws-modules/vpc/aws"  (registry)&lt;/span&gt;

  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.20.0.0/16"&lt;/span&gt;
  &lt;span class="nx"&gt;az_count&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
  &lt;span class="nx"&gt;enable_flow_logs&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"private_subnet_ids"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_ids&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The contract is the whole idea: &lt;strong&gt;variables are the questions the module asks, outputs are the promises it makes, and everything in between is its private business.&lt;/strong&gt; A caller should never need to read a module's internals to use it — the moment they do, the abstraction has leaked and the module has failed at its one job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing the interface (where modules are won or lost)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Expose decisions, not resources.&lt;/strong&gt; Callers should pass intent — "3 AZs, flow logs on, this CIDR" — not raw resource arguments copied from the AWS provider docs. If your variable list mirrors the underlying resource's argument list one-to-one, you have not written a module; you have written a forwarding service. The module's value &lt;em&gt;is&lt;/em&gt; its opinions: the subnet layout, the tag set, the naming scheme, the secure defaults — decided once, encoded once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Defaults carry the opinions.&lt;/strong&gt; A good module can be called with three or four inputs by a team that trusts its defaults:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"az_count"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"How many availability zones to spread subnets across."&lt;/span&gt;
  &lt;span class="nx"&gt;validation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;az_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;az_count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
    &lt;span class="nx"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"az_count must be between 2 and 4 for this network design."&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"enable_flow_logs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bool&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;# secure by default; turning it off is a decision, not an accident&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the shape: typed variables, a description a stranger can act on, validation that fails at &lt;em&gt;plan&lt;/em&gt; time with a human sentence, and defaults that encode the safe choice. Every default you set is a caller who never has to think about that knob — and a team convention that can never be forgotten.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Outputs are the composition layer.&lt;/strong&gt; Publish the identifiers other modules will need (subnet IDs, the cluster endpoint, the bucket ARN) and nothing else. Outputs are a public API: teams will build on whatever you publish, so publish deliberately, and treat removing an output as a breaking change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Size: one coherent thing
&lt;/h2&gt;

&lt;p&gt;The temptation is the mega-module — &lt;code&gt;module "platform"&lt;/code&gt; that builds the network, the cluster, the databases and the applications in one call. It feels convenient and ages terribly: every consumer inherits every decision, a change to any layer re-plans all of them, and the state file becomes a blast radius the size of your whole estate.&lt;/p&gt;

&lt;p&gt;The working rule: &lt;strong&gt;a module wraps one layer of the stack that changes together and gets consumed together.&lt;/strong&gt; Network. Cluster. Data store. Application service. Those compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"network"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./modules/network"&lt;/span&gt;  &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"cluster"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./modules/cluster"&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_ids&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./modules/app-service"&lt;/span&gt;
  &lt;span class="nx"&gt;cluster_id&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;image&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"registry.example.com/api:${var.api_version}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Composition through outputs keeps each module independently testable, independently versionable, and independently &lt;em&gt;replaceable&lt;/em&gt; — when the cluster design changes, the network module neither knows nor cares. The same thinking, incidentally, is why &lt;a href="https://www.devopsenginer.com/blog/terraform-state-management-guide-2026" rel="noopener noreferrer"&gt;state management&lt;/a&gt; per layer keeps blast radius small: module boundaries and state boundaries want to be the same lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Versioning: modules are software, treat them that way
&lt;/h2&gt;

&lt;p&gt;A module consumed from a git repo at &lt;code&gt;?ref=main&lt;/code&gt; is an unpinned dependency on a moving target: every consumer silently upgrades whenever anyone merges anything. The first time that merge changes a default, every environment in the company plans a diff nobody asked for — during somebody's unrelated change window.&lt;/p&gt;

&lt;p&gt;The disciplined version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tag releases semantically.&lt;/strong&gt; &lt;code&gt;v1.4.0&lt;/code&gt; tells consumers what kind of change they're taking: patch for fixes, minor for additive inputs/outputs, major for breaking changes (renamed variables, changed defaults, removed outputs).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin every reference.&lt;/strong&gt; &lt;code&gt;?ref=v1.4.0&lt;/code&gt;, never a branch. Upgrading a module becomes a deliberate, one-line, reviewable act per environment — staging takes it this week, production next week, and the diff in between is the changelog.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep a changelog.&lt;/strong&gt; Three honest lines per release beats a generated novel: what changed, who is affected, what to check after upgrading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test before tagging.&lt;/strong&gt; A module's test surface is &lt;code&gt;terraform validate&lt;/code&gt;, a plan against a sandbox account, and — for the modules that matter — an ephemeral apply-and-destroy in CI. The registry ecosystem (&lt;code&gt;terraform test&lt;/code&gt;, now built in) supports real test runs with mocked providers for the logic-heavy modules. Whatever the depth, a release nobody has planned against is a beta, labelled or not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Local path modules (&lt;code&gt;./modules/...&lt;/code&gt;) skip versioning entirely — fine for one repo's internal layers, where the "consumers" ship in the same commit. The moment a module is consumed from &lt;em&gt;another&lt;/em&gt; repo or by &lt;em&gt;another team&lt;/em&gt;, it has users, and users deserve versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registry modules: stand on the community carefully
&lt;/h2&gt;

&lt;p&gt;The public registry holds mature modules for the common shapes — the AWS VPC module alone encodes years of edge cases. Using them is good engineering, with two cautions. First, read the interface before committing: a famous module with 200 input variables demands you learn &lt;em&gt;its&lt;/em&gt; opinions, which may not be yours. Second, a registry module is still a dependency: pin its version, and re-check its defaults on upgrade (a default flip in a popular module has surprised more than one team). When a registry module fits, it saves months; when it almost fits, a thin in-house wrapper module — your interface on the outside, the registry module inside — keeps your callers insulated from its surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The god-module.&lt;/strong&gt; Network + cluster + data + apps in one call: unreviewable plans, maximal blast radius, and consumers forced to accept every layer's choices. Split by layer; compose through outputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variables that mirror resource arguments.&lt;/strong&gt; A module whose inputs are the provider's docs restated provides no leverage — the opinions &lt;em&gt;are&lt;/em&gt; the product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unpinned sources.&lt;/strong&gt; &lt;code&gt;ref=main&lt;/code&gt; turns every merge into a fleet-wide event. Pin versions; upgrade deliberately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything is an output.&lt;/strong&gt; Outputs are API. Publishing internals invites consumers to build on them, and then "internal" refactors become breaking changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One module for every environment difference.&lt;/strong&gt; A variable count that grows with every new consumer is a god-module assembling itself. When a consumer's shape is genuinely different, a second module (or composition of smaller ones) beats a flag for every fork in the road.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No validation, failure at apply time.&lt;/strong&gt; An invalid CIDR or an az_count of 7 should fail in seconds at plan, with a sentence explaining why — not fifteen minutes into an apply, midway through creating a network.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When should code become a module?&lt;/strong&gt;&lt;br&gt;
At the second honest reuse — or the first, if the thing is a recognised layer (network, cluster) whose shape you already know. Extracting on first use for everything produces speculative abstractions; waiting until the fourth copy produces archaeology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local modules or a shared module repo?&lt;/strong&gt;&lt;br&gt;
One repo's layers: local paths, shipped together. Modules with consumers in other repos or teams: their own repo (or a registry), versioned and released. The boundary is about who consumes it, not about elegance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I refactor a module without breaking consumers?&lt;/strong&gt;&lt;br&gt;
The same way you change any API: additively first (new optional variables with defaults, new outputs), deprecate in the changelog, and reserve removals/renames for a major version. Consumers pinned to the old major are untouched until they choose to move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modules vs just using Terragrunt/workspaces?&lt;/strong&gt;&lt;br&gt;
Different layers of the problem. Workspaces and wrapper tools manage &lt;em&gt;environments and repetition at the root level&lt;/em&gt;; modules manage &lt;em&gt;reusable components&lt;/em&gt;. Most estates of any size end up with both, and the state guidance in the &lt;a href="https://www.devopsenginer.com/blog/terraform-state-management-guide-2026" rel="noopener noreferrer"&gt;state management guide&lt;/a&gt; applies either way. If you're still choosing the IaC tool itself, the &lt;a href="https://www.devopsenginer.com/blog/terraform-vs-pulumi-cloudformation-2025-iac-comparison" rel="noopener noreferrer"&gt;Terraform vs Pulumi vs CloudFormation comparison&lt;/a&gt; is the wider view.&lt;/p&gt;

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

&lt;p&gt;A Terraform module is a promise: give me these few decisions, and I will give you infrastructure built the way this team agreed — every time, in every environment, from one definition. Keep the interface small and opinionated, compose layers through outputs, version like software, and pin like you mean it. Do that, and the tenth environment costs you an afternoon of variables instead of a week of copy-paste — which was the entire point.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Setting up your first environments? The Kubernetes Production Checklist and the other free resources are on &lt;a href="https://www.devopsenginer.com" rel="noopener noreferrer"&gt;DevOps Enginer&lt;/a&gt; — new practical guides are published weekly.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>iac</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Kubernetes ConfigMaps and Secrets in 2026: Configuration Without Rebuilding Images</title>
      <dc:creator>softverseAdmin</dc:creator>
      <pubDate>Sat, 31 Oct 2026 08:10:22 +0000</pubDate>
      <link>https://dev.to/softverseadmin/kubernetes-configmaps-and-secrets-in-2026-configuration-without-rebuilding-images-2gk7</link>
      <guid>https://dev.to/softverseadmin/kubernetes-configmaps-and-secrets-in-2026-configuration-without-rebuilding-images-2gk7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; ConfigMaps hold non-sensitive configuration (feature flags, endpoints, tuning values); Secrets hold sensitive values (passwords, tokens, keys) — with one warning that must be said out loud: a Kubernetes Secret is only base64-encoded, not encrypted, so anyone with API read access to it has the value. Configuration reaches pods two ways — environment variables (simple, but frozen at pod start) and volume mounts (files the kubelet keeps updated, but only for apps that re-read them). The production pattern most teams converge on: keep config in ConfigMaps, keep real secrets in an external vault (AWS Secrets Manager, Vault, and friends) and sync them in with the External Secrets Operator, encrypt etcd at rest, scope access with RBAC, and treat rotation as a designed event, not an emergency. Configuration is where "works in staging" goes to die — this guide makes it boring instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.devopsenginer.com/blog/kubernetes-configmaps-secrets-guide-2026" rel="noopener noreferrer"&gt;DevOps Enginer&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Rebuild an image to change a log level once, and you understand why configuration exists as its own discipline. Rebuild one to change a database password during an incident, and you never design config badly again. Kubernetes gives you two objects for this job — ConfigMaps and Secrets — and a decade of teams have learned the same few lessons about them. Here they are, in one place.&lt;/p&gt;

&lt;h2&gt;
  
  
  ConfigMaps: the plain-text workhorse
&lt;/h2&gt;

&lt;p&gt;A ConfigMap is key-value data, nothing more exotic:&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;ConfigMap&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;app-config&lt;/span&gt;
&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;info"&lt;/span&gt;
  &lt;span class="na"&gt;FEATURE_CHECKOUT_V2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
  &lt;span class="na"&gt;app.properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;pool.size=20&lt;/span&gt;
    &lt;span class="s"&gt;pool.timeout=5s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It can carry simple values and whole files (the &lt;code&gt;app.properties&lt;/code&gt; block above lands as a file when mounted). Creation from files or literals via &lt;code&gt;kubectl create configmap&lt;/code&gt; or your GitOps pipeline — as with everything else on this site, the manifest in Git is the version worth having; ad-hoc &lt;code&gt;kubectl&lt;/code&gt; creates are config drift with extra steps. ConfigMaps are namespace-scoped, capped around 1 MiB, and — importantly — they are &lt;em&gt;not&lt;/em&gt; a database: hundreds of tiny ConfigMaps are a smell pointing at a config service or a file baked sensibly into the repo, not more objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets: base64 is not encryption (the paragraph to actually read)
&lt;/h2&gt;

&lt;p&gt;A Secret looks like a ConfigMap with &lt;code&gt;type: Opaque&lt;/code&gt; and values that appear encoded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db-credentials&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Opaque&lt;/span&gt;
&lt;span class="na"&gt;stringData&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;          &lt;span class="c1"&gt;# write plaintext here; the API server encodes it&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3cr3t-value"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That encoding is &lt;strong&gt;base64 — a transport format, not protection&lt;/strong&gt;. The defences that actually protect a Secret, in the order they matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;RBAC:&lt;/strong&gt; who can &lt;code&gt;get&lt;/code&gt;/&lt;code&gt;list&lt;/code&gt; Secrets in the namespace. Read access to a Secret &lt;em&gt;is&lt;/em&gt; the secret. This is the control that does the most work, and it is covered properly in the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-security-basics-guide-2026" rel="noopener noreferrer"&gt;security basics guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encryption at rest:&lt;/strong&gt; etcd stores Secrets; enable envelope encryption (KMS-backed on managed clusters, often a toggle) so a stolen etcd backup doesn't hand over every credential in the cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not committing plaintext Secrets to Git.&lt;/strong&gt; The most violated rule in this entire article. If manifests live in Git — they should — Secrets in plaintext manifests means the Git repo &lt;em&gt;is&lt;/em&gt; your vault, with Git's access list, clone history and all. The fixes are the next section.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Secret types beyond &lt;code&gt;Opaque&lt;/code&gt; exist for specific consumers (TLS secrets for ingress, docker-registry secrets for image pulls), and those integrate with their consumers directly — an ingress TLS secret, for instance, is the standard way to terminate HTTPS per the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-ingress-networking-basics-2026" rel="noopener noreferrer"&gt;networking guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting configuration into the pod: env vars vs files
&lt;/h2&gt;

&lt;p&gt;Two delivery mechanisms, with opposite trade-offs:&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;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;LOG_LEVEL&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="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;app-config&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;LOG_LEVEL&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;DB_PASSWORD&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;secretKeyRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;db-credentials&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;password&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config&lt;/span&gt;
        &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/app&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config&lt;/span&gt;
      &lt;span class="na"&gt;configMap&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;app-config&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables&lt;/strong&gt; are set at container start and &lt;em&gt;never change for the life of the pod&lt;/em&gt;. Update the ConfigMap and nothing happens until pods restart. Simple, predictable, and the right default for values that shouldn't mutate under a running process anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume mounts&lt;/strong&gt; present the data as files, and the kubelet updates the files when the source changes (within a minute or so, via an atomic symlink swap). But an updated file only matters if the application re-reads it — most don't. So volume-mounted config gives you &lt;em&gt;eventual files&lt;/em&gt;, not &lt;em&gt;live reload&lt;/em&gt;, unless the app watches the files (some frameworks do, via the &lt;code&gt;..data&lt;/code&gt; symlink change).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The operational consequence people trip on: &lt;strong&gt;changing a ConfigMap does not roll your Deployment.&lt;/strong&gt; If the change must take effect, trigger the rollout (&lt;code&gt;kubectl rollout restart&lt;/code&gt;) — or use the Helm trick of checksumming the config into a pod-template annotation so a config change &lt;em&gt;is&lt;/em&gt; a template change and rolls automatically, per the &lt;a href="https://www.devopsenginer.com/blog/helm-charts-tutorial-2026" rel="noopener noreferrer"&gt;Helm guide&lt;/a&gt;. Decide deliberately which behaviour each config deserves; "sometimes it reloads, sometimes not" is the worst of both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets in Git, solved properly: external stores and sync
&lt;/h2&gt;

&lt;p&gt;Since plaintext Secrets must not live in Git, but workloads need Secrets in the cluster, the industry converged on one architecture: &lt;strong&gt;the vault is the source of truth; a controller syncs values into the cluster as Secrets.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The common implementation is the &lt;strong&gt;External Secrets Operator (ESO)&lt;/strong&gt;: you commit an &lt;code&gt;ExternalSecret&lt;/code&gt; manifest that contains &lt;em&gt;references&lt;/em&gt; — no values — and ESO fetches the real values from AWS Secrets Manager, GCP Secret Manager, Azure Key Vault or HashiCorp Vault using the cluster's own cloud identity:&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;external-secrets.io/v1beta1&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;ExternalSecret&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-credentials&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;refreshInterval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1h&lt;/span&gt;
  &lt;span class="na"&gt;secretStoreRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;aws-secrets&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;ClusterSecretStore&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;db-credentials&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;secretKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password&lt;/span&gt;
      &lt;span class="na"&gt;remoteRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;prod/app/db&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;property&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;password&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What Git shows anyone: that a password exists and where it lives. What it never shows: the password. Rotation happens in the vault; ESO refreshes the Secret; pods pick it up on their next restart (or sooner, for the file-watchers). Alternatives in the same family: the cloud providers' own CSI drivers (mounting secrets as in-memory files, never persisted as API objects at all — arguably the cleanest model) and &lt;strong&gt;Sealed Secrets&lt;/strong&gt;, which encrypts values with a cluster key so the ciphertext is safe to commit — simpler than a vault, with key management becoming your job instead. For a small team with no vault yet, Sealed Secrets is a legitimate step; for anything multi-cluster or compliance-flavoured, the external-store pattern wins.&lt;/p&gt;

&lt;p&gt;Two hygiene rules whichever route you choose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Different secrets per environment.&lt;/strong&gt; The staging credential and the production credential being identical is how a staging compromise becomes a production incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit access.&lt;/strong&gt; Secret reads should be logged (CloudTrail on the vault side, API audit logs in-cluster) — per the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-observability-guide-2026" rel="noopener noreferrer"&gt;observability guide&lt;/a&gt;, the time to learn who read a secret is not during the postmortem.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rotation: the event you design before it designs you
&lt;/h2&gt;

&lt;p&gt;Credentials rotate — on a schedule if you're organised, during an incident if you're not. A rotation that requires editing manifests in three repos and restarting everything by hand is not a rotation procedure; it's a hope. The designed version:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Vault holds the new value alongside the old (most stores support versions; databases can hold two valid users during a transition window).&lt;/li&gt;
&lt;li&gt;ESO syncs; a controlled rollout restarts pods against the new value (&lt;code&gt;kubectl rollout restart&lt;/code&gt;, staged by &lt;a href="https://www.devopsenginer.com/blog/kubernetes-deployment-strategies-2025-complete-guide" rel="noopener noreferrer"&gt;deployment strategy&lt;/a&gt; so a bad credential fails a canary, not the fleet).&lt;/li&gt;
&lt;li&gt;Old value revoked &lt;em&gt;after&lt;/em&gt; the new one is verified serving traffic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice what this procedure never involves: a human seeing the value, a value in a chat message, or a rebuild. If any of those appear in your rotation story, the design has a hole in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Plaintext Secrets in Git.&lt;/strong&gt; The classic. History keeps them forever, every clone keeps them everywhere. Vault + sync, or sealed ciphertext — those are the options.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Believing base64 is encryption.&lt;/strong&gt; It is encoding, readable by anyone in milliseconds. RBAC and etcd encryption are the actual protections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One giant shared Secret for everything.&lt;/strong&gt; Every pod that mounts it can read all of it. Split by consumer: a pod should hold the credentials for its own job and nothing else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expecting ConfigMap edits to reload apps.&lt;/strong&gt; Env vars never update; mounted files update but apps rarely re-read. Roll the pods, or wire the checksum annotation, deliberately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same credentials in every environment&lt;/strong&gt; — and in the load-test cluster, and in that old namespace nobody deleted. Scope per environment, per workload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config in the image, secrets in the wiki.&lt;/strong&gt; Config baked into images forces rebuilds for tuning; secrets documented in shared notes are secrets in name only. Both are the same mistake: the value lives where it isn't managed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ConfigMap or Secret for a database URL?&lt;/strong&gt;&lt;br&gt;
The URL itself (host, port, database name) is configuration — ConfigMap. The username and password are Secrets. Splitting them is normal and makes rotation cleaner: credentials change without touching the endpoint config.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How big can a ConfigMap be?&lt;/strong&gt;&lt;br&gt;
Around 1 MiB per object (etcd object limits). If your config approaches that, it isn't config anymore — it's data, and it belongs in object storage or a database, mounted or fetched by the app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should pods read config from the API directly instead?&lt;/strong&gt;&lt;br&gt;
Almost never. It couples the app to Kubernetes APIs and permissions it shouldn't hold. Env vars and files are the interface; keep the app portable and the cluster's API unexposed to workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between Sealed Secrets and External Secrets?&lt;/strong&gt;&lt;br&gt;
Sealed Secrets stores the encrypted value in Git and decrypts it in-cluster — the repo remains the distribution mechanism. External Secrets stores only a reference in Git and fetches the value from a vault at sync time. The vault model scales to rotation, audit and multi-cluster; the sealed model is simpler to adopt. Both beat plaintext.&lt;/p&gt;

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

&lt;p&gt;Configuration is a design surface, not a chore: ConfigMaps for the values you'd print on a whiteboard, Secrets for the ones you wouldn't, RBAC and etcd encryption as the real protections, a vault with sync as the source of truth, and rotation as a rehearsed procedure. Get those five decisions right and config changes become the least interesting part of your week — which, for anything involving production credentials, is exactly the ambition.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The free &lt;a href="https://www.devopsenginer.com" rel="noopener noreferrer"&gt;Kubernetes Production Checklist&lt;/a&gt; (via the newsletter on the homepage) includes the configuration and secrets pre-flight items — RBAC scoping, backup tests and rotation readiness — alongside the rest of the production readiness checks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>secrets</category>
      <category>configuration</category>
      <category>devops</category>
    </item>
    <item>
      <title>Kubernetes Upgrades in 2026: Minor Versions Without the Downtime or the Drama</title>
      <dc:creator>softverseAdmin</dc:creator>
      <pubDate>Sat, 31 Oct 2026 07:57:27 +0000</pubDate>
      <link>https://dev.to/softverseadmin/kubernetes-upgrades-in-2026-minor-versions-without-the-downtime-or-the-drama-17fk</link>
      <guid>https://dev.to/softverseadmin/kubernetes-upgrades-in-2026-minor-versions-without-the-downtime-or-the-drama-17fk</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Kubernetes releases three minor versions a year, and each is supported for roughly a year — fall two versions behind and upgrading stops being routine maintenance and becomes a project. Safe upgrades follow a fixed order: check for removed APIs before you touch anything, upgrade the control plane first (one minor version at a time, never skip), then rotate nodes with surge capacity so workloads always have somewhere to land, respecting PodDisruptionBudgets the whole way. The two facts that shape everything: you cannot skip minor versions on the control plane, and there is no downgrade button — your rollback plan is an etcd backup and node images you can still boot. Do the preparation and an upgrade is a boring afternoon. Skip it, and the boring afternoon finds you anyway, at a worse time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.devopsenginer.com/blog/kubernetes-upgrades-guide-2026" rel="noopener noreferrer"&gt;DevOps Enginer&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every cluster is quietly ageing. The Kubernetes release train does not stop because a team is busy, and the gap between "we should upgrade soon" and "our version just went end-of-life" is where upgrade projects turn into incidents. This guide is the upgrade as a routine: the checks, the order, and the traps — written for the person who wants upgrades to be boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules of the road
&lt;/h2&gt;

&lt;p&gt;Four constraints govern every upgrade, and planning means accepting them rather than negotiating:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One minor version at a time for the control plane.&lt;/strong&gt; Going 1.30 → 1.32 directly is not supported for kube-apiserver. Each step gets applied, verified, and only then does the next begin. (Managed control planes enforce this for you; the steps still happen.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version skew is a policy, not a vibe.&lt;/strong&gt; The supported combinations are specific: kubelets may be up to three minor versions behind the API server (since 1.28), but never newer than it; &lt;code&gt;kubectl&lt;/code&gt; works within one minor version either way. "It connected" is not the same as "it's supported."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APIs get removed.&lt;/strong&gt; Every minor release can retire beta APIs. Workloads still referencing a removed API fail at apply time — or worse, at the moment you need to redeploy during an unrelated incident. This is what the preparation phase exists to catch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is no downgrade.&lt;/strong&gt; The API server and etcd do not roll back. If an upgrade goes wrong mid-flight, recovery means restoring etcd from backup or rebuilding — which is why backups are step zero, not an afterthought.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Phase 1: Preparation (where upgrades are actually won)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Inventory the target.&lt;/strong&gt; Read the target version's changelog and the release's "urgent upgrade notes," and check your add-ons against it: ingress controller, CNI, CSI drivers, cert-manager, cluster-autoscaler/Karpenter, metrics-server. Add-on incompatibility breaks more upgrades than Kubernetes itself does — an ingress controller that doesn't support the new version will not announce this until traffic stops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hunt for removed APIs.&lt;/strong&gt; Two tools make this mechanical:&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;# Scans manifests / Helm releases for deprecated and removed APIs&lt;/span&gt;
pluto detect-helm &lt;span class="nt"&gt;-o&lt;/span&gt; wide
pluto detect-files &lt;span class="nt"&gt;-d&lt;/span&gt; ./manifests &lt;span class="nt"&gt;-o&lt;/span&gt; wide

&lt;span class="c"&gt;# Scans the live cluster for APIs in use that are scheduled for removal&lt;/span&gt;
kubent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix every finding by moving to the replacement API (the tools name it) &lt;em&gt;before&lt;/em&gt; the upgrade window. While you are in there, check the same hygiene for admission webhooks: a dead webhook with &lt;code&gt;failurePolicy: Fail&lt;/code&gt; can block the whole cluster's writes after a control-plane change, so inventory webhooks and know which ones are load-bearing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Back up etcd — and know how you'd restore it.&lt;/strong&gt; On self-managed clusters, snapshot etcd and copy the snapshot off the node. On managed clusters (EKS, GKE, AKS), the provider handles the control plane, but your &lt;em&gt;workload state&lt;/em&gt; is your problem: if everything is in Git (see my &lt;a href="https://www.devopsenginer.com/blog/argo-cd-tutorial-gitops-2026" rel="noopener noreferrer"&gt;Argo CD tutorial&lt;/a&gt;), the cluster is rebuildable; if it isn't, a volume-level backup tool is your safety net, per the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-storage-guide-2026" rel="noopener noreferrer"&gt;storage guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check cluster health honestly.&lt;/strong&gt; Upgrading a cluster that already has failing nodes, Pending pods, or a flapping control plane converts one problem into two. &lt;code&gt;kubectl get nodes&lt;/code&gt;, component statuses, and a look at recent events: fix first, upgrade second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2: Control plane first
&lt;/h2&gt;

&lt;p&gt;With preparation done, the control plane goes first — always. On managed services this is one action per minor version (console, CLI or Terraform), followed by a pause: watch API server availability, controller-manager and scheduler health, and confirm nodes still report Ready before proceeding.&lt;/p&gt;

&lt;p&gt;On self-managed clusters with kubeadm, the shape is:&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;# On the first control-plane node, per minor version:&lt;/span&gt;
apt-mark unhold kubeadm &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nv"&gt;kubeadm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.31.x-&lt;span class="k"&gt;*&lt;/span&gt;   &lt;span class="c"&gt;# target version&lt;/span&gt;
kubeadm upgrade plan
kubeadm upgrade apply v1.31.x
&lt;span class="c"&gt;# Then upgrade kubelet + kubectl on the node and restart kubelet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kubeadm upgrade plan&lt;/code&gt; is the unsung hero here: it reports component versions, warns about what it can and cannot do, and refuses some genuinely bad ideas. Read its output like a pre-flight checklist, because that is what it is. Repeat for the remaining control-plane nodes with &lt;code&gt;kubeadm upgrade node&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 3: Nodes — rotate, don't patch in place
&lt;/h2&gt;

&lt;p&gt;Worker nodes upgrade by replacement wherever possible: bring up a node at the new version, drain an old one, let workloads reschedule, remove the old node. On managed node groups this is the built-in rolling update, with one setting that decides whether it is graceful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Max surge:&lt;/strong&gt; allow the group to temporarily run &lt;em&gt;extra&lt;/em&gt; nodes (e.g. +1 or +33%) during the update. Surge means a drained node's workloads already have a home; without surge, the update cordons a node and hopes capacity suffices — on a tightly packed cluster, it doesn't, and pods sit Pending mid-upgrade. This is the same capacity logic as &lt;a href="https://www.devopsenginer.com/blog/kubernetes-autoscaling-guide-2026" rel="noopener noreferrer"&gt;autoscaling&lt;/a&gt;: replacement needs somewhere to land.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PodDisruptionBudgets are the contract&lt;/strong&gt; that makes draining safe: drain respects voluntary-disruption budgets, so a workload with &lt;code&gt;minAvailable&lt;/code&gt; set cannot be fully evicted by an upgrade. Workloads &lt;em&gt;without&lt;/em&gt; PDBs can be taken to zero by a drain — if that outage would hurt, the PDB was the missing pre-work (Phase 1 again).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The manual shape, when you are rotating nodes yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl cordon &amp;lt;old-node&amp;gt;
kubectl drain &amp;lt;old-node&amp;gt; &lt;span class="nt"&gt;--ignore-daemonsets&lt;/span&gt; &lt;span class="nt"&gt;--delete-emptydir-data&lt;/span&gt;
&lt;span class="c"&gt;# wait: confirm every workload is Running elsewhere&lt;/span&gt;
kubectl delete node &amp;lt;old-node&amp;gt;   &lt;span class="c"&gt;# after the node itself is terminated/replaced&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch for the two classic drain blockers: pods with local storage (&lt;code&gt;--delete-emptydir-data&lt;/code&gt; acknowledges losing it — be sure you mean that) and pods whose PDB forbids the eviction, in which case the drain waits, correctly, and you investigate the workload rather than forcing it. &lt;code&gt;--force&lt;/code&gt; exists; reaching for it during an upgrade is how upgrades become incidents.&lt;/p&gt;

&lt;p&gt;On self-managed nodes where replacement isn't practical, kubelet upgrades in place: install the new kubelet (and container runtime, checking its own compatibility), restart, verify the node rejoins at the new version within skew policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 4: Verify like you don't trust it (because you shouldn't)
&lt;/h2&gt;

&lt;p&gt;"Upgrade completed" from a console is the beginning of verification, not the end:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;kubectl version&lt;/code&gt; and &lt;code&gt;kubectl get nodes&lt;/code&gt; — every node at the intended version, all Ready.&lt;/li&gt;
&lt;li&gt;Core add-ons healthy: DNS resolution from a test pod, ingress serving, metrics flowing (&lt;code&gt;kubectl top nodes&lt;/code&gt;) — the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-observability-guide-2026" rel="noopener noreferrer"&gt;observability guide&lt;/a&gt; lists the signals worth watching in the hours after.&lt;/li&gt;
&lt;li&gt;One real deployment: roll a canary workload, confirm scheduling, networking and storage attach all behave.&lt;/li&gt;
&lt;li&gt;Add-on upgrades you deferred: now is when ingress, cert-manager and friends get their matching bumps, one at a time, while the window is still staffed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then leave the cluster alone for a soak period before starting the &lt;em&gt;next&lt;/em&gt; minor version. One version, one verification, one soak — the rhythm that keeps a three-version catch-up from becoming a single three-version mystery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managed vs self-managed: what actually differs
&lt;/h2&gt;

&lt;p&gt;The order and the rules are identical; only the hands change. Managed services move the control plane for you and offer node-group rotation with surge built in — your job concentrates on Phase 1 (API removals, add-on compatibility) and Phase 4 (verification), which is honestly where the risk always lived. Self-managed adds the kubeadm mechanics and etcd backup duty. In both worlds, the teams that get hurt are hurt by preparation gaps, not by the upgrade command itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Skipping the removed-API scan.&lt;/strong&gt; Everything works until the first redeploy against a removed API — during a different incident, naturally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trying to jump two minor versions.&lt;/strong&gt; The control plane refuses or, worse, half-applies. One step at a time is not a suggestion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero surge on node rotation.&lt;/strong&gt; Drained workloads with nowhere to go; Pending pods; an upgrade window that ends in a rollback of the schedule rather than the version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No PDBs on important workloads.&lt;/strong&gt; Drain takes them to zero, voluntarily, as designed — the design just never got told they mattered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upgrading an unhealthy cluster.&lt;/strong&gt; Pre-existing node or control-plane faults become upgrade casualties and misdirect the whole investigation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting the add-ons.&lt;/strong&gt; Kubernetes upgrades fine; the two-year-old ingress controller doesn't. Inventory includes everything with a version number, not just Kubernetes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How often should we upgrade?&lt;/strong&gt;&lt;br&gt;
Often enough to never be more than one minor version behind the oldest supported release — in practice, most teams settle into upgrading every 2–4 months, which keeps each event small. The cost of upgrading grows with the distance jumped; frequency is the discount.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can we skip a version on managed Kubernetes?&lt;/strong&gt;&lt;br&gt;
No — the provider performs the same one-minor-at-a-time steps; the console just makes them look like one action. Node groups likewise step through versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if something breaks mid-upgrade?&lt;/strong&gt;&lt;br&gt;
For nodes: stop the rotation, keep workloads on healthy nodes, fix forward or replace the bad node. For the control plane: there is no downgrade, which is why the pre-upgrade etcd snapshot (self-managed) or your Git-backed workload state (everywhere) is the real rollback plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do workloads need downtime?&lt;/strong&gt;&lt;br&gt;
Properly built ones don't: multiple replicas, PDBs, surge capacity and rolling drains mean pods move, not stop. Single-replica workloads without PDBs will have a gap during their node's drain — the upgrade is merely revealing a property the workload always had. The &lt;a href="https://www.devopsenginer.com/blog/kubernetes-deployment-strategies-2025-complete-guide" rel="noopener noreferrer"&gt;deployment strategies guide&lt;/a&gt; covers building workloads that survive exactly this.&lt;/p&gt;

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

&lt;p&gt;A Kubernetes upgrade is a checklist wearing a scary costume: scan for removed APIs, back up, control plane one minor at a time, nodes rotated with surge and PDBs respected, then verify like a sceptic. Run it a few times a year and it shrinks to what it should be — routine maintenance with a beginning, a middle, and an end. Let it slide for a year, and the same work returns wearing its real costume: a project, with a deadline set by an end-of-life date.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The free &lt;a href="https://www.devopsenginer.com" rel="noopener noreferrer"&gt;Kubernetes Production Checklist&lt;/a&gt; (via the newsletter on the homepage) includes the upgrade pre-flight items — health checks, backup verification and disruption budgets — alongside the rest of the production readiness checks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>upgrades</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Kubernetes Storage in 2026: PersistentVolumes, StorageClasses and StatefulSets Without the Fear</title>
      <dc:creator>softverseAdmin</dc:creator>
      <pubDate>Sat, 31 Oct 2026 07:39:02 +0000</pubDate>
      <link>https://dev.to/softverseadmin/kubernetes-storage-in-2026-persistentvolumes-storageclasses-and-statefulsets-without-the-fear-2pcg</link>
      <guid>https://dev.to/softverseadmin/kubernetes-storage-in-2026-persistentvolumes-storageclasses-and-statefulsets-without-the-fear-2pcg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Kubernetes storage confuses people because it splits one idea — "my pod needs a disk" — into three objects. A PersistentVolumeClaim (PVC) is the pod's request: "I need 10Gi, writable by one node." A PersistentVolume (PV) is the actual disk that satisfies it. A StorageClass is the recipe that creates those disks on demand, with a chosen type, reclaim behaviour and expansion policy. Stateless apps should stay on Deployments with no volumes at all. Apps that genuinely own data — databases, queues, anything whose identity is its disk — belong on StatefulSets, which give each replica a stable name, a stable network identity and its own claim that survives rescheduling. Most storage incidents come from three places: the wrong reclaim policy deleting data, a volume bound to one availability zone while the pod reschedules into another, and nobody testing a restore until the day it matters.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.devopsenginer.com/blog/kubernetes-storage-guide-2026" rel="noopener noreferrer"&gt;DevOps Enginer&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Pods are cattle; disks are pets. Everything awkward about Kubernetes storage lives in that sentence. This guide walks the moving parts in the order a request flows through them, then covers StatefulSets, expansion, snapshots, and the failure modes that actually bite.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three objects, in the order they meet
&lt;/h2&gt;

&lt;p&gt;When a pod declares it needs storage, this is the chain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;PersistentVolumeClaim&lt;/strong&gt; — the request, written by you (or generated by a StatefulSet). It asks for a size, an access mode, and optionally a StorageClass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;StorageClass&lt;/strong&gt; — the template. It names the provisioner (your cloud's disk service, a CSI driver, a local provisioner), the parameters (disk type, IOPS tier), the reclaim policy, and whether volumes can be expanded later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PersistentVolume&lt;/strong&gt; — the reality. With dynamic provisioning (the normal case on any cloud cluster), the StorageClass's provisioner creates a real disk and a PV object representing it, bound one-to-one to your claim.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You almost never create PVs by hand anymore. You write PVCs, you choose (or set a default) StorageClass, and provisioning does the rest. If a claim sits in &lt;code&gt;Pending&lt;/code&gt;, the conversation between those three objects broke somewhere — usually the class doesn't exist, the provisioner can't create disks in that zone, or quota ran out. &lt;code&gt;kubectl describe pvc &amp;lt;name&amp;gt;&lt;/code&gt; prints the events that tell you which.&lt;/p&gt;

&lt;h2&gt;
  
  
  Access modes: the line that decides your architecture
&lt;/h2&gt;

&lt;p&gt;Every claim declares how the volume may be mounted, and this choice constrains everything downstream:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ReadWriteOnce (RWO)&lt;/strong&gt; — one node at a time. The default for cloud block disks (EBS, Azure Disk, GCE PD). Fine for a database with one writer; it also means a rolling update can deadlock if the new pod lands on a different node while the old pod still holds the volume — the classic multi-attach error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ReadWriteMany (RWX)&lt;/strong&gt; — many nodes at once. Requires a shared filesystem (NFS, EFS, Azure Files, Filestore). Needed when several pods across nodes must see the same files; you pay for it in performance characteristics and complexity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ReadWriteOncePod (RWOP)&lt;/strong&gt; — one &lt;em&gt;pod&lt;/em&gt;, not just one node (Kubernetes 1.29+ for CSI volumes). The stricter guarantee single-writer workloads actually wanted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read the mode before you design around a volume. Half of all "Kubernetes storage is broken" stories are a workload assuming RWX semantics on an RWO disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  StorageClasses: the decisions hiding in the defaults
&lt;/h2&gt;

&lt;p&gt;One cluster usually offers several classes — a fast SSD class, a balanced class, maybe an HDD class. The fields that matter most are the quiet ones:&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;storage.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;StorageClass&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;fast-retain&lt;/span&gt;
&lt;span class="na"&gt;provisioner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ebs.csi.aws.com&lt;/span&gt;        &lt;span class="c1"&gt;# example: AWS EBS CSI driver&lt;/span&gt;
&lt;span class="na"&gt;parameters&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;gp3&lt;/span&gt;
&lt;span class="na"&gt;reclaimPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Retain&lt;/span&gt;               &lt;span class="c1"&gt;# not Delete — see below&lt;/span&gt;
&lt;span class="na"&gt;volumeBindingMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WaitForFirstConsumer&lt;/span&gt;
&lt;span class="na"&gt;allowVolumeExpansion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;reclaimPolicy&lt;/code&gt;&lt;/strong&gt; decides what happens to the disk when the PVC is deleted. &lt;code&gt;Delete&lt;/code&gt; destroys the underlying volume — convenient for throwaway environments, catastrophic for a production database whose claim gets deleted by a namespace cleanup or a careless Helm uninstall. For anything whose data matters: &lt;code&gt;Retain&lt;/code&gt;. Retain leaves the PV in a Released state; the data survives until a human deliberately deals with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;volumeBindingMode: WaitForFirstConsumer&lt;/code&gt;&lt;/strong&gt; delays provisioning until a pod actually schedules. Without it, a disk can be created in zone A before the scheduler places the pod in zone B — and a zonal disk can never attach across zones, so the pod is stuck forever. This single field prevents one of the most common storage dead-ends on multi-zone clusters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;allowVolumeExpansion: true&lt;/code&gt;&lt;/strong&gt; is only settable at class creation in effect — you cannot retroactively make a non-expandable class expandable for existing volumes in most setups. Set it now, thank yourself later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check what your cluster defaults to before writing a single claim:&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 storageclass
kubectl describe pvc &amp;lt;claim&amp;gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &amp;lt;namespace&amp;gt;
kubectl get pv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  StatefulSets: when the pod's identity IS the disk
&lt;/h2&gt;

&lt;p&gt;Deployments treat replicas as interchangeable — perfect for stateless services, wrong for a database cluster where &lt;code&gt;db-0&lt;/code&gt;, &lt;code&gt;db-1&lt;/code&gt; and &lt;code&gt;db-2&lt;/code&gt; are different members with different data. StatefulSets exist for those workloads and give three guarantees Deployments don't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stable identity:&lt;/strong&gt; pods are named &lt;code&gt;name-0&lt;/code&gt;, &lt;code&gt;name-1&lt;/code&gt;, … and keep that name across restarts, with a matching stable DNS entry per pod via a headless Service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-replica storage:&lt;/strong&gt; a &lt;code&gt;volumeClaimTemplates&lt;/code&gt; block generates one PVC per replica (&lt;code&gt;data-db-0&lt;/code&gt;, &lt;code&gt;data-db-1&lt;/code&gt;, …). When a pod is deleted and recreated, it reattaches to &lt;em&gt;its&lt;/em&gt; claim — the data follows the identity, not the node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ordered behaviour:&lt;/strong&gt; replicas start and stop in order, which clustered systems that elect leaders and replicate in sequence genuinely need.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="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;StatefulSet&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;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;serviceName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db-headless&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&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="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;db&lt;/span&gt; &lt;span class="pi"&gt;}&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="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;db&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&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;postgres:16&lt;/span&gt;
          &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;data&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/lib/postgresql/data&lt;/span&gt;
  &lt;span class="na"&gt;volumeClaimTemplates&lt;/span&gt;&lt;span class="pi"&gt;:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;data&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;accessModes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ReadWriteOnce"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;storageClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fast-retain&lt;/span&gt;
        &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two operational truths about StatefulSet claims. First, &lt;strong&gt;scaling down does not delete the claims&lt;/strong&gt; — scale from 3 back to 5 later and the old replicas reattach to their existing data. That is deliberate and usually what you want; it also means forgotten claims quietly keep disks (and bills) alive, which is why storage belongs in the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-cost-optimization-guide-2026" rel="noopener noreferrer"&gt;cost optimization&lt;/a&gt; review. Second, deleting a StatefulSet leaves its PVCs behind too — cleanup is a separate, explicit act. Annoying on a tidy-up day; a lifesaver on a bad day.&lt;/p&gt;

&lt;p&gt;And the honest question to ask first: does this workload need to be stateful &lt;em&gt;in Kubernetes at all&lt;/em&gt;? A managed database service outside the cluster is often the right answer for small teams. Run state in-cluster when you have a real reason — latency, cost at scale, data locality — not by default. If you do run it, the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-deployment-strategies-2025-complete-guide" rel="noopener noreferrer"&gt;deployment strategies guide&lt;/a&gt; covers how updates roll for these workloads, and it is not "like a Deployment."&lt;/p&gt;

&lt;h2&gt;
  
  
  Growing and protecting volumes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Expansion&lt;/strong&gt; is the happy path, when it was prepared: with &lt;code&gt;allowVolumeExpansion: true&lt;/code&gt; on the class, you edit the PVC's requested size upward and the provisioner grows the disk; most filesystems expand online once the pod restarts or remounts. Shrinking is not supported — a smaller disk means a new volume and a data migration, full stop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshots&lt;/strong&gt; are the backup primitive: a &lt;code&gt;VolumeSnapshot&lt;/code&gt; asks the CSI driver for a point-in-time copy, and a new PVC can be created &lt;em&gt;from&lt;/em&gt; a snapshot to restore or clone. Combined with a schedule (via your backup tooling or a CronJob-driven process), this is how in-cluster state gets real backups. Two warnings from experience-shaped convention:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A snapshot is not a backup strategy until a &lt;strong&gt;restore has been tested&lt;/strong&gt;. Untested restores fail in creative ways: wrong class, wrong zone, permissions, application-level inconsistency.&lt;/li&gt;
&lt;li&gt;For databases, storage-level snapshots are crash-consistent at best unless the application quiesces writes. Where correctness matters, pair snapshots with the database's own backup tooling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt; applies to disks too: volume contents inherit your &lt;a href="https://www.devopsenginer.com/blog/kubernetes-security-basics-guide-2026" rel="noopener noreferrer"&gt;RBAC and Pod Security&lt;/a&gt; posture — whoever can create a PVC from a snapshot can read the data in it, so snapshot permissions deserve the same least-privilege care as the volumes themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;reclaimPolicy: Delete&lt;/code&gt; on data that matters.&lt;/strong&gt; The claim gets deleted — by a person, a script, a namespace teardown — and the disk goes with it. Retain for production state, always.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;WaitForFirstConsumer&lt;/code&gt; on multi-zone clusters.&lt;/strong&gt; Disks provisioned in the wrong zone; pods Pending forever with attach errors that look like scheduler problems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming RWX from an RWO disk.&lt;/strong&gt; Multi-attach errors during rollouts, or worse, silent assumptions in the app design that only surface under failover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting StatefulSet claims on cleanup.&lt;/strong&gt; Orphaned PVs holding real disks for months — visible in the bill, invisible in &lt;code&gt;kubectl get pods&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshots nobody has restored from.&lt;/strong&gt; A backup you have never tested is a hypothesis. Schedule a restore drill like you schedule the backup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running stateful workloads on Deployments with a shared claim.&lt;/strong&gt; Losing stable identity and per-replica storage is how databases end up with two writers and one corrupted dataset.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Should my web app have a PersistentVolume?&lt;/strong&gt;&lt;br&gt;
Almost never. If the app is stateless, uploads and user files belong in object storage (S3/GCS/Azure Blob) or a managed service, not a pod-local disk. PVCs are for workloads whose data is the workload — databases, brokers, search indexes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens to my data if the pod is deleted?&lt;/strong&gt;&lt;br&gt;
With a PVC, nothing — the volume outlives the pod and reattaches to its replacement. With no volume (emptyDir or the container filesystem), everything is gone with the pod. That distinction is the entire subject of this article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I move a volume to another cluster?&lt;/strong&gt;&lt;br&gt;
Not by copying YAML. The path is snapshot/restore (CSI snapshots, or backup tools built on them) or application-level replication/export. Plan migrations around the data tooling, not the manifests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is my PVC stuck in Pending?&lt;/strong&gt;&lt;br&gt;
Read its events: &lt;code&gt;kubectl describe pvc&lt;/code&gt;. The usual causes are a StorageClass that doesn't exist, a provisioner that can't create volumes in the needed zone, exhausted quota, or &lt;code&gt;WaitForFirstConsumer&lt;/code&gt; waiting (correctly) for a pod that itself can't schedule — in which case the pod's events, covered in the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-troubleshooting-guide-2026" rel="noopener noreferrer"&gt;troubleshooting guide&lt;/a&gt;, hold the real reason.&lt;/p&gt;

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

&lt;p&gt;Kubernetes storage stops being scary when you keep the chain straight: claims ask, classes define, volumes provide — and StatefulSets bind identity to data for the workloads that need it. Choose the access mode honestly, set Retain and WaitForFirstConsumer before they save you, grow volumes instead of replacing them, and test restores before you need them. Disks are pets; treat them with the deliberate care pets require, and they will stop biting.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The free &lt;a href="https://www.devopsenginer.com" rel="noopener noreferrer"&gt;Kubernetes Production Checklist&lt;/a&gt; (via the newsletter on the homepage) includes the storage pre-flight items — reclaim policies, backup tests and disruption budgets — alongside the rest of the production readiness checks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>storage</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Kubernetes Security Basics in 2026: RBAC, Network Policies and Pod Security That Actually Get Used</title>
      <dc:creator>softverseAdmin</dc:creator>
      <pubDate>Sat, 31 Oct 2026 07:13:33 +0000</pubDate>
      <link>https://dev.to/softverseadmin/kubernetes-security-basics-in-2026-rbac-network-policies-and-pod-security-that-actually-get-used-3fa3</link>
      <guid>https://dev.to/softverseadmin/kubernetes-security-basics-in-2026-rbac-network-policies-and-pod-security-that-actually-get-used-3fa3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; A default Kubernetes cluster is far more open than most teams realise: any pod can talk to any other pod, many workloads run with more API permission than they need, and "the app works" often quietly means "the app runs as root with a token mounted it never uses." You do not need a security platform to fix the worst of it. Three controls close most of the real-world holes: RBAC scoped to what each identity actually does, NetworkPolicies that end the flat any-to-any network, and Pod Security Standards that stop privileged, root-running workloads at admission time. Add disciplined secrets handling and image hygiene, and a default cluster becomes a defensible one — with changes your team can roll out namespace by namespace, without a big-bang migration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.devopsenginer.com/blog/kubernetes-security-basics-guide-2026" rel="noopener noreferrer"&gt;DevOps Enginer&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes security advice tends to arrive as a 200-item checklist, which is why so much of it never gets implemented. This guide takes the opposite approach: the small set of controls that remove the most risk per hour of effort, in the order I would apply them to a real cluster, with the manifests that matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start from the uncomfortable defaults
&lt;/h2&gt;

&lt;p&gt;Before the controls, be honest about what a fresh cluster permits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The network is flat.&lt;/strong&gt; With no NetworkPolicies, every pod can open a connection to every other pod and service in the cluster. One compromised frontend can reach your database directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every pod gets a service account token&lt;/strong&gt; mounted by default (unless you turn it off), whether or not the application ever calls the Kubernetes API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Containers frequently run as root&lt;/strong&gt;, with the default capabilities, on images pulled by a mutable tag like &lt;code&gt;latest&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RBAC is often bypassed in practice&lt;/strong&gt; — not by attackers first, but by teams handing out &lt;code&gt;cluster-admin&lt;/code&gt; because a narrower role "took too long to debug."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exotic. Attackers do not need zero-days when the defaults hand them lateral movement and credentials. The good news: each default above has a specific, boring fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control 1: RBAC — least privilege for humans and workloads
&lt;/h2&gt;

&lt;p&gt;RBAC answers one question: &lt;em&gt;who is allowed to do what to which resources?&lt;/em&gt; Two identities matter — the people (and CI systems) who operate the cluster, and the service accounts workloads run as.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For workloads:&lt;/strong&gt; give each application its own ServiceAccount, and set &lt;code&gt;automountServiceAccountToken: false&lt;/code&gt; unless the app genuinely calls the API. Most applications never do — the mounted token is pure attack surface: steal the pod, steal a credential.&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;ServiceAccount&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
&lt;span class="na"&gt;automountServiceAccountToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an app &lt;em&gt;does&lt;/em&gt; need API access, bind a Role that names the exact verbs and resources — &lt;code&gt;get&lt;/code&gt; and &lt;code&gt;list&lt;/code&gt; on &lt;code&gt;configmaps&lt;/code&gt; in one namespace is a role; &lt;code&gt;*&lt;/code&gt; on &lt;code&gt;*&lt;/code&gt; is an incident waiting for a date:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config-reader&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;production&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;configmaps"&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;list"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;watch"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For humans and CI:&lt;/strong&gt; scope roles per namespace, treat &lt;code&gt;cluster-admin&lt;/code&gt; like root on a production database — a named, audited exception, not a team default — and audit what you have. Two commands tell you most of the story:&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 clusterrolebindings &lt;span class="nt"&gt;-o&lt;/span&gt; wide
kubectl auth can-i &lt;span class="nt"&gt;--list&lt;/span&gt; &lt;span class="nt"&gt;--as&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;system:serviceaccount:production:api &lt;span class="nt"&gt;-n&lt;/span&gt; production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second is the one to run for every workload identity: it prints exactly what that token can do, in plain text. If the list surprises you, that is the finding. This is the same least-privilege discipline as IAM on the cloud side — my &lt;a href="https://www.devopsenginer.com/blog/aws-iam-least-privilege-guide-2026" rel="noopener noreferrer"&gt;AWS IAM least privilege guide&lt;/a&gt; works through the identical thinking for AWS policies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control 2: NetworkPolicies — end the flat network
&lt;/h2&gt;

&lt;p&gt;A NetworkPolicy is a firewall rule for pods: it selects pods and declares which traffic may reach them (ingress) and where they may connect (egress). Policies are enforced by your CNI plugin — an important caveat: if your CNI does not enforce NetworkPolicy (the basic kubenet setup is the classic example), the objects exist and silently do nothing. Cilium, Calico and the major cloud CNIs enforce them; verify yours before trusting the YAML.&lt;/p&gt;

&lt;p&gt;Roll out in this order, namespace by namespace:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — default deny.&lt;/strong&gt; One policy that selects all pods in the namespace and allows nothing changes the posture from "open unless closed" to "closed unless opened":&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;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;default-deny&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;production&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="pi"&gt;{}&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ingress"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Egress"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2 — allow what the app actually needs.&lt;/strong&gt; Then add narrow allowances: the frontend may reach the API on its port; the API may reach the database on 5432 and DNS on 53 (forgetting DNS egress is the classic self-inflicted outage — allow it explicitly):&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;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;api-ingress&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;production&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;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
  &lt;span class="na"&gt;policyTypes&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;Ingress"&lt;/span&gt;&lt;span class="pi"&gt;]&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;app&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;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this in a staging namespace first, watch what breaks, and promote the same policies with the application — if you run GitOps, they belong in the same repository as the workloads, reviewed like any other change (my &lt;a href="https://www.devopsenginer.com/blog/argo-cd-tutorial-gitops-2026" rel="noopener noreferrer"&gt;Argo CD tutorial&lt;/a&gt; covers that workflow). A compromised pod inside a default-deny namespace is a contained problem; in a flat network it is a cluster tour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control 3: Pod Security Standards — stop bad workloads at the door
&lt;/h2&gt;

&lt;p&gt;RBAC limits identities and NetworkPolicies limit traffic, but neither stops a deployment that asks to run privileged, mount the host filesystem, or run as root. That is admission-time work, and Kubernetes has it built in: &lt;strong&gt;Pod Security Standards&lt;/strong&gt;, enforced through Pod Security Admission with three profiles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privileged&lt;/strong&gt; — effectively no restrictions. For system components that genuinely need host access, and nothing else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Baseline&lt;/strong&gt; — blocks the known privilege escalations (privileged containers, host namespaces, dangerous capabilities) while staying compatible with most typical workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restricted&lt;/strong&gt; — the hardened target: no root, no privilege escalation, drop all capabilities, seccomp on. What user-facing workloads should aim for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enforcement is a namespace label — no webhook to install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl label namespace production &lt;span class="se"&gt;\&lt;/span&gt;
  pod-security.kubernetes.io/enforce&lt;span class="o"&gt;=&lt;/span&gt;baseline &lt;span class="se"&gt;\&lt;/span&gt;
  pod-security.kubernetes.io/audit&lt;span class="o"&gt;=&lt;/span&gt;restricted &lt;span class="se"&gt;\&lt;/span&gt;
  pod-security.kubernetes.io/warn&lt;span class="o"&gt;=&lt;/span&gt;restricted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That combination is the pragmatic rollout: &lt;strong&gt;enforce Baseline today&lt;/strong&gt; (violating pods are rejected), while &lt;strong&gt;audit and warn on Restricted&lt;/strong&gt; shows you — in API warnings and audit logs — exactly which workloads would fail the stricter profile, so you can fix them deliberately instead of breaking production on day one. At the workload level, the settings Restricted asks for are small and belong in every Deployment template:&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;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runAsNonRoot&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;runAsUser&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10001&lt;/span&gt;
  &lt;span class="na"&gt;allowPrivilegeEscalation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;drop&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;ALL"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;seccompProfile&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;RuntimeDefault&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The honest blocker is usually the image: an app whose image only runs as root needs a Dockerfile fix, not an exception. Treat exceptions as named, expiring decisions — a namespace labelled back to Privileged "temporarily" has a way of becoming permanent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets: the rules that prevent the embarrassing incidents
&lt;/h2&gt;

&lt;p&gt;Kubernetes Secrets are base64-encoded, not encrypted, and anyone with the right RBAC verb can read them — which loops straight back to Control 1. The working rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encryption at rest on&lt;/strong&gt; for etcd (a cluster-level setting; managed clusters differ in defaults — verify yours, don't assume).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never bake secrets into images or commit them to Git.&lt;/strong&gt; Use environment injection or mounted volumes from a secrets source, and scan repositories in CI so a committed credential fails the pipeline — that scanning habit is part of the wider pipeline discipline in my &lt;a href="https://www.devopsenginer.com/blog/devsecops-checklist-securing-pipeline-2025" rel="noopener noreferrer"&gt;DevSecOps checklist&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer an external secrets store&lt;/strong&gt; (cloud secret managers, or an external-secrets operator syncing from one) as the source of truth, so rotation and audit live in one place instead of being scattered across namespaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate like you mean it.&lt;/strong&gt; A secret that has never been rotated should be assumed permanent — and anything permanent eventually leaks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Image hygiene: know what you are actually running
&lt;/h2&gt;

&lt;p&gt;The last basic: the artefact you deploy. Four habits cover most of it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pin by digest, or at least by specific version tag.&lt;/strong&gt; &lt;code&gt;latest&lt;/code&gt; means "whatever exists at pull time," which is neither reproducible nor auditable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scan in CI and watch the results.&lt;/strong&gt; A scanner that reports into a void changes nothing; gate on fixable critical vulnerabilities for internet-facing workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use minimal bases.&lt;/strong&gt; A small base image is fewer packages to patch and fewer surprises — the same discipline as my &lt;a href="https://www.devopsenginer.com/blog/how-to-optimize-docker-images-production-2025" rel="noopener noreferrer"&gt;Docker image optimization guide&lt;/a&gt;, where smaller and more secure turned out to be the same work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull from registries you control or trust&lt;/strong&gt;, with image pull policies that do not silently fall back to stale local copies.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;NetworkPolicies on a CNI that doesn't enforce them.&lt;/strong&gt; The YAML applies cleanly, everyone feels safer, nothing is filtered. Verify enforcement with a test connection that &lt;em&gt;should&lt;/em&gt; fail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cluster-admin&lt;/code&gt; as a convenience.&lt;/strong&gt; Every standing cluster-admin binding is a standing invitation. Scope it, name it, review it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default-deny without DNS egress.&lt;/strong&gt; The policies "work" and every lookup fails. Always allow UDP/TCP 53 to your DNS service explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforcing Restricted on day one.&lt;/strong&gt; Rejected pods across production, followed by a panicked rollback to no standards at all. Baseline-enforce + Restricted-audit gets you to the same place without the outage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service account tokens mounted everywhere.&lt;/strong&gt; One flag — &lt;code&gt;automountServiceAccountToken: false&lt;/code&gt; — removes a credential from every pod that never needed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets in Git "just for staging."&lt;/strong&gt; Staging secrets have a habit of being production secrets. Scan, block, rotate if it already happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Where do I start if the cluster has none of this?&lt;/strong&gt;&lt;br&gt;
In this order: inventory RBAC (&lt;code&gt;clusterrolebindings&lt;/code&gt; and &lt;code&gt;can-i --list&lt;/code&gt; for your workloads), turn off automatic token mounting where the API isn't used, then default-deny NetworkPolicies in one staging namespace, and Baseline Pod Security in audit mode cluster-wide. Each step is an afternoon, not a quarter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do NetworkPolicies replace a service mesh or an external firewall?&lt;/strong&gt;&lt;br&gt;
No — they cover pod-to-pod traffic inside the cluster at L3/L4. Egress to the internet, L7 rules and identity-based policy are other layers' jobs. NetworkPolicies are the floor, not the whole building.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managed Kubernetes (EKS/GKE/AKS) — is this handled for me?&lt;/strong&gt;&lt;br&gt;
Partially. The provider secures the control plane and offers defaults, but RBAC bindings, NetworkPolicies, Pod Security labels and your images remain yours. The shared-responsibility line runs right through the middle of this article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I prove it's working?&lt;/strong&gt;&lt;br&gt;
Test negatively: from a frontend pod, try to reach the database and watch it fail; deploy a privileged test pod in a Baseline namespace and watch it get rejected. A control you have never seen block anything is a hope, not a control. For the wider detection picture, see the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-observability-guide-2026" rel="noopener noreferrer"&gt;Kubernetes observability guide&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;Kubernetes security basics are not a product you buy; they are five defaults you deliberately change: scoped RBAC, no free tokens, a network that denies by default, admission that refuses privileged workloads, and secrets and images handled like they matter. Roll them out namespace by namespace, enforce gradually, and test that each control actually blocks something. A cluster secured that way will not make headlines — which is precisely the point.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The free &lt;a href="https://www.devopsenginer.com" rel="noopener noreferrer"&gt;Kubernetes Production Checklist&lt;/a&gt; (via the newsletter on the homepage) includes the security pre-flight items — RBAC review, token mounting, disruption budgets and rollback paths — alongside the rest of the production readiness checks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>security</category>
      <category>devsecops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Kubernetes Autoscaling in 2026: HPA, VPA, Cluster Autoscaling and KEDA — What Each One Actually Does</title>
      <dc:creator>softverseAdmin</dc:creator>
      <pubDate>Sat, 31 Oct 2026 06:33:54 +0000</pubDate>
      <link>https://dev.to/softverseadmin/kubernetes-autoscaling-in-2026-hpa-vpa-cluster-autoscaling-and-keda-what-each-one-actually-does-237n</link>
      <guid>https://dev.to/softverseadmin/kubernetes-autoscaling-in-2026-hpa-vpa-cluster-autoscaling-and-keda-what-each-one-actually-does-237n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Kubernetes has four different autoscaling layers and most problems come from expecting one of them to do another one's job. The Horizontal Pod Autoscaler (HPA) changes how many pods you run. The Vertical Pod Autoscaler (VPA) changes how big each pod is. Cluster autoscaling (Cluster Autoscaler or Karpenter) changes how many nodes exist for pods to land on. KEDA scales on events — queue depth, Kafka lag, cron schedules — instead of CPU. In production you usually need at least two of these together: HPA for replicas plus node autoscaling underneath, or your new pods sit Pending forever. And none of them work without honest resource requests, because every percentage-based decision is calculated from the requests you set.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.devopsenginer.com/blog/kubernetes-autoscaling-guide-2026" rel="noopener noreferrer"&gt;DevOps Enginer&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Getting autoscaling right is the difference between a cluster that absorbs a traffic spike quietly and one that pages you at 3 a.m. while pods pile up unscheduled. This guide walks each layer in the order traffic actually flows through your cluster, with the configuration that matters and the mistakes that quietly break each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why autoscaling fails quietly
&lt;/h2&gt;

&lt;p&gt;Autoscaling rarely fails with an error. It fails silently, in three classic ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pods scale, nodes don't.&lt;/strong&gt; HPA adds replicas, but there is no room for them. They sit in &lt;code&gt;Pending&lt;/code&gt; while the load that triggered them keeps growing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nodes scale, pods can't use them.&lt;/strong&gt; The cluster adds capacity, but pod requests are set so high that each node fits only one or two pods — you pay for nodes that are mostly reserved air.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The metric lies.&lt;/strong&gt; HPA targets 70% CPU, but the app is memory-bound, or requests were set years ago and no longer resemble reality. The scaler makes confident decisions from bad inputs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every layer below exists to close one of these gaps. If you haven't set requests and limits deliberately yet, start with my &lt;a href="https://www.devopsenginer.com/blog/kubernetes-resource-requests-limits-guide-2026" rel="noopener noreferrer"&gt;guide to Kubernetes resource requests and limits&lt;/a&gt; — autoscaling built on guessed requests just automates the guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: Horizontal Pod Autoscaler — more copies of the pod
&lt;/h2&gt;

&lt;p&gt;HPA is the layer most teams already have. It watches a metric — by default CPU or memory utilisation relative to pod requests — and adjusts the replica count of a Deployment, StatefulSet or similar workload between a minimum and a maximum.&lt;/p&gt;

&lt;p&gt;A production-shaped HPA looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;autoscaling/v2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HorizontalPodAutoscaler&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scaleTargetRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
  &lt;span class="na"&gt;minReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&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;20&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;65&lt;/span&gt;
  &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;scaleUp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;stabilizationWindowSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
      &lt;span class="na"&gt;policies&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;Percent&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
          &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
    &lt;span class="na"&gt;scaleDown&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;stabilizationWindowSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
      &lt;span class="na"&gt;policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pods&lt;/span&gt;
          &lt;span class="na"&gt;value&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;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details in that manifest do most of the work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;minReplicas: 3&lt;/code&gt;, not 1.&lt;/strong&gt; A single replica is a single point of failure and a slow cold start on the first spike. Three gives the scaler something to work with and survives a node loss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;behavior&lt;/code&gt; block.&lt;/strong&gt; Without it, scale-down is eager: load dips, replicas vanish, load returns, pods churn. A 300-second stabilisation window on scale-down and one pod per minute keeps the replica count calm instead of flapping. Scale-up stays aggressive — when you need capacity, you need it now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The target is utilisation of requests.&lt;/strong&gt; 65% of a 500m CPU request is a real number; 65% of a request nobody has reviewed since 2023 is not. This is why the requests guide matters more than the HPA manifest itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prerequisites people forget: HPA needs metrics. Resource metrics come from the &lt;strong&gt;metrics-server&lt;/strong&gt; add-on, which many clusters install by default but minimal or self-managed clusters may not have. If &lt;code&gt;kubectl top pods&lt;/code&gt; returns nothing, HPA is flying blind and will do nothing — check that first before debugging the HPA object itself. Custom and external metrics (requests per second, queue length) need an adapter such as the Prometheus Adapter, which is where KEDA, below, often becomes the simpler path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: Vertical Pod Autoscaler — the right size for each pod
&lt;/h2&gt;

&lt;p&gt;VPA attacks a different problem: the pod itself is the wrong size. It observes actual usage over time and recommends — or automatically applies — better requests.&lt;/p&gt;

&lt;p&gt;VPA runs in three modes, and choosing the mode is the whole decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Off&lt;/strong&gt; — it only shows recommendations (&lt;code&gt;kubectl describe vpa&lt;/code&gt; becomes a right-sizing report). Safe everywhere, and honestly the mode most teams should start with for a few weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Initial&lt;/strong&gt; — requests are set when a pod is created and never changed afterwards. Low disruption, slow to adapt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto&lt;/strong&gt; — VPA evicts and recreates pods to apply new requests. Correct sizes, but your pods restart when VPA decides — never point Auto mode at something that cannot tolerate restarts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The classic conflict:&lt;/strong&gt; never let VPA in Auto mode and HPA fight over the same resource. If HPA scales on CPU utilisation while VPA keeps resizing the requests that utilisation is calculated from, the two controllers chase each other. The standard peace treaty: HPA on CPU (or on custom metrics), VPA in Off mode as an advisor — or VPA adjusting memory only while HPA watches CPU. My &lt;a href="https://www.devopsenginer.com/blog/kubernetes-cost-optimization-guide-2026" rel="noopener noreferrer"&gt;Kubernetes cost optimization guide&lt;/a&gt; covers why this right-sizing loop is where most of the money is: oversized requests are a silent tax on every node you run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: Node autoscaling — room for the pods to land on
&lt;/h2&gt;

&lt;p&gt;HPA can decide it wants 20 replicas, but if the cluster has capacity for 8, the other 12 do not exist yet — they are &lt;code&gt;Pending&lt;/code&gt;. Node autoscaling is the layer that notices unschedulable pods and adds machines, and later removes empty ones.&lt;/p&gt;

&lt;p&gt;You have two mainstream choices:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cluster Autoscaler&lt;/strong&gt; is the older, conservative option. It watches for pending pods, asks the cloud provider for another node from an existing node group or instance pool, and scales groups down when nodes sit under-utilised. It is predictable and battle-tested, and its weakness is rigidity: it scales within node groups &lt;em&gt;you&lt;/em&gt; pre-defined, so the instance types and sizes are your homework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Karpenter&lt;/strong&gt; (open source, originally built at AWS and now a CNCF project) takes the opposite approach: instead of scaling fixed groups, it watches pending pods, reads their actual requirements — CPU, memory, zones, labels, taints — and provisions a node shaped to fit them, often in under a minute. When nodes empty out, it consolidates: shuffles pods onto fewer, cheaper nodes and terminates the rest. The practical difference shows up exactly where Cluster Autoscaler is rigid — mixed workloads with very different shapes — and in consolidation, which steadily squeezes wasted capacity out of the cluster instead of only reacting to spikes.&lt;/p&gt;

&lt;p&gt;Whichever you run, three settings decide whether it behaves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PodDisruptionBudgets on important workloads.&lt;/strong&gt; Node scale-down and consolidation drain nodes. A PDB is how a workload says "you may move me, but never take me below this many healthy replicas." Without PDBs, autoscaling will eventually evict something at a bad moment. (My &lt;a href="https://www.devopsenginer.com/blog/kubernetes-deployment-strategies-2025-complete-guide" rel="noopener noreferrer"&gt;deployment strategies guide&lt;/a&gt; covers PDBs alongside rolling updates.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do-not-evict annotations for the truly pinned pods&lt;/strong&gt; — the singleton, the job mid-run. Both controllers respect signals that a pod must not be moved; use them sparingly and deliberately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A floor under the cluster.&lt;/strong&gt; Keep enough baseline capacity for your steady state. Autoscaling is for variance, not for running the cluster at zero slack and hoping provisioning is always fast.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Layer 4: KEDA — scaling on events, not CPU
&lt;/h2&gt;

&lt;p&gt;Some workloads have no meaningful CPU signal. A worker that drains a queue is idle at 2% CPU with 10,000 messages waiting; a report generator should exist twice a day and never at night. Scaling these on CPU is scaling on a lie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KEDA&lt;/strong&gt; (Kubernetes Event-Driven Autoscaling, a CNCF graduated project) extends HPA rather than replacing it: it feeds external signals — SQS or Kafka backlog, RabbitMQ queue depth, database query results, cron schedules, Prometheus queries — into the scaling decision as metrics. Its two signature abilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scale to zero.&lt;/strong&gt; HPA's floor is 1 replica (well, &lt;code&gt;minReplicas&lt;/code&gt; of 1 in practice). KEDA can take a workload to 0 when the queue is empty and bring it back when work arrives. For intermittent workers, this is the difference between paying for idle pods all month and paying for none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale on the signal that predicts the work.&lt;/strong&gt; Queue depth rises &lt;em&gt;before&lt;/em&gt; CPU does. Scaling on the cause instead of the symptom removes most of the lag that makes CPU-based scaling feel sluggish.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A typical pattern: a &lt;code&gt;ScaledObject&lt;/code&gt; declares the target deployment, the trigger (say, messages visible in a queue), and a threshold (one pod per 50 messages, between 0 and 15 replicas). KEDA maintains the underlying HPA for you. If you already run &lt;a href="https://www.devopsenginer.com/blog/argo-cd-tutorial-gitops-2026" rel="noopener noreferrer"&gt;Argo CD&lt;/a&gt;, ScaledObjects are just more YAML in Git — the same review-and-rollback story as everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting the layers together
&lt;/h2&gt;

&lt;p&gt;A sane default stack for a typical web service:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Decision it makes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pods&lt;/td&gt;
&lt;td&gt;HPA&lt;/td&gt;
&lt;td&gt;How many replicas, from CPU/custom metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pod size&lt;/td&gt;
&lt;td&gt;VPA (Off mode)&lt;/td&gt;
&lt;td&gt;Whether requests match reality — advisory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nodes&lt;/td&gt;
&lt;td&gt;Karpenter or Cluster Autoscaler&lt;/td&gt;
&lt;td&gt;How many machines, from pending pods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Events&lt;/td&gt;
&lt;td&gt;KEDA (where relevant)&lt;/td&gt;
&lt;td&gt;Scaling workers/jobs on queues and schedules&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the observability to prove it works: watch &lt;code&gt;Pending&lt;/code&gt; pod count, HPA's current-vs-desired replicas, and node utilisation after consolidation. If you cannot see autoscaling deciding, you are trusting it blindly — the metrics to wire up are covered in my &lt;a href="https://www.devopsenginer.com/blog/kubernetes-observability-guide-2026" rel="noopener noreferrer"&gt;Kubernetes observability guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Autoscaling on CPU for a memory-bound app.&lt;/strong&gt; The metric must be the resource the workload actually runs out of — or a custom metric closer to the real demand, like requests per second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No metrics-server, no scaling.&lt;/strong&gt; &lt;code&gt;kubectl top&lt;/code&gt; returning nothing means HPA sees nothing. Verify the plumbing before tuning the policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HPA + VPA Auto on the same resource.&lt;/strong&gt; Two controllers, one variable, endless churn. Pick who owns CPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;maxReplicas as an afterthought.&lt;/strong&gt; Too low and the spike wins; absurdly high and a runaway loop or a bad deploy can scale you into a very large cloud bill. The max is a budget decision, not a formality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling pods without scaling nodes.&lt;/strong&gt; The &lt;code&gt;Pending&lt;/code&gt; pile-up. Every HPA rollout should be paired with the question: where do the new pods land?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No PodDisruptionBudgets.&lt;/strong&gt; Node consolidation and scale-down will eventually evict the wrong pod at the wrong time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale-down as eager as scale-up.&lt;/strong&gt; Flapping replicas churn connections, caches and readiness. Stabilisation windows are free stability.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need all four layers?&lt;/strong&gt;&lt;br&gt;
No. Most services need HPA plus node autoscaling. Add VPA in advisory mode when you suspect requests are wrong (they usually are), and KEDA when a workload's real demand signal is a queue, a schedule or another external event rather than CPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Karpenter or Cluster Autoscaler?&lt;/strong&gt;&lt;br&gt;
If you are starting fresh on AWS, Karpenter's just-in-time, right-shaped provisioning and consolidation usually win. Cluster Autoscaler remains a solid, predictable choice — especially outside AWS or where node groups are deliberately fixed for compliance reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can KEDA replace HPA?&lt;/strong&gt;&lt;br&gt;
KEDA &lt;em&gt;uses&lt;/em&gt; HPA under the hood for most triggers. What it replaces is your hand-rolled metrics plumbing, and it adds scale-to-zero, which plain HPA does not give you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why are my pods Pending even though autoscaling is on?&lt;/strong&gt;&lt;br&gt;
Work through the layers: HPA may have scaled (check desired replicas), but if nodes are full and the node autoscaler cannot add capacity — quota limits, no matching node group, Karpenter constraints — pods wait. &lt;code&gt;kubectl describe pod&lt;/code&gt; on a pending pod names the failing condition; my &lt;a href="https://www.devopsenginer.com/blog/kubernetes-troubleshooting-guide-2026" rel="noopener noreferrer"&gt;Kubernetes troubleshooting guide&lt;/a&gt; walks that diagnosis step by step.&lt;/p&gt;

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

&lt;p&gt;Autoscaling is not one feature you turn on; it is four decisions stacked on top of each other — how many pods, how big each pod is, how many nodes exist, and which signal you trust. Get the requests honest first, pair every pod scaler with node capacity underneath it, protect workloads with disruption budgets, and scale event-driven work on the events themselves. Do that, and traffic spikes become a graph you glance at — not an incident you get paged for.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Running autoscaling in production? The &lt;a href="https://www.devopsenginer.com" rel="noopener noreferrer"&gt;Kubernetes Production Checklist&lt;/a&gt; (free PDF via the newsletter on the homepage) covers the pre-flight checks — requests, probes, disruption budgets and rollback paths — that make scaling safe to automate.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>autoscaling</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Argo Rollouts 2026: Progressive Delivery for Kubernetes — Canary, Blue-Green and Rollbacks That Happen by Themselves</title>
      <dc:creator>softverseAdmin</dc:creator>
      <pubDate>Sat, 31 Oct 2026 04:49:00 +0000</pubDate>
      <link>https://dev.to/softverseadmin/argo-rollouts-2026-progressive-delivery-for-kubernetes-canary-blue-green-and-rollbacks-that-163</link>
      <guid>https://dev.to/softverseadmin/argo-rollouts-2026-progressive-delivery-for-kubernetes-canary-blue-green-and-rollbacks-that-163</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; A plain Kubernetes Deployment can roll an update out, but it cannot send 5% of traffic to the new version, watch the error rate, and roll itself back when the numbers turn bad. Argo Rollouts can. It replaces your Deployment with a Rollout resource that adds weighted canary steps, blue-green switching, metric-driven analysis, and automatic rollback — and it slots neatly next to Argo CD if you already run GitOps. This guide installs it, ships a first canary, wires metrics into the promotion decision, and covers the mistakes that bite.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.devopsenginer.com/blog/argo-rollouts-progressive-delivery-2026" rel="noopener noreferrer"&gt;DevOps Enginer&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My &lt;a href="https://www.devopsenginer.com/blog/kubernetes-deployment-strategies-2025-complete-guide" rel="noopener noreferrer"&gt;Kubernetes deployment strategies guide&lt;/a&gt; explains the patterns: rolling, blue-green, canary, recreate. Patterns are the easy half. The hard half is execution — who watches the new version while it serves its first real traffic, and who has the nerve (and the speed) to pull it back at 2 a.m. when the error graph twitches?&lt;/p&gt;

&lt;p&gt;A stock Deployment answers "nobody, really." It replaces pods on a schedule of percentages of &lt;em&gt;replicas&lt;/em&gt;, not traffic. It has no idea whether the new version is healthy in any sense that matters to users. Rollback is a human noticing, deciding, and typing.&lt;/p&gt;

&lt;p&gt;Argo Rollouts is the controller that closes that gap. And if you already run Argo CD — I do, and my &lt;a href="https://www.devopsenginer.com/blog/argo-cd-tutorial-gitops-2026" rel="noopener noreferrer"&gt;Argo CD tutorial&lt;/a&gt; walks through it — Rollouts is from the same Argo family and feels like the missing half: Argo CD gets the right version &lt;em&gt;to&lt;/em&gt; the cluster; Rollouts decides how that version &lt;em&gt;earns&lt;/em&gt; the rest of the traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a plain Deployment is not enough
&lt;/h2&gt;

&lt;p&gt;Three limits show up the first time a release goes wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Replica math is not traffic math.&lt;/strong&gt; "25% of pods are new" does not mean 25% of users see the new version — behind a Service, it is roughly proportional, but you cannot set 1%, or 5%, or hold a step deliberately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is no verify step.&lt;/strong&gt; A Deployment promotes on pod readiness. A pod can be Ready and still return 500s, leak memory, or take 4 seconds per request. Readiness is not health.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback is manual and slow.&lt;/strong&gt; &lt;code&gt;kubectl rollout undo&lt;/code&gt; works — after a human has noticed, paged, decided, and typed. The blast radius grows the whole time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Progressive delivery is the discipline of fixing exactly these three: small traffic steps, automated verification between steps, and rollback that does not wait for a human. That is what Rollouts implements.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Argo Rollouts actually is
&lt;/h2&gt;

&lt;p&gt;Three pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Rollouts controller&lt;/strong&gt; — a deployment in your cluster that watches Rollout resources and drives ReplicaSets, Services, and (optionally) your ingress or mesh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Rollout resource&lt;/strong&gt; — a near drop-in replacement for a Deployment. Same pod template, same selectors; the difference is the &lt;code&gt;strategy&lt;/code&gt; block, which describes &lt;em&gt;how&lt;/em&gt; versions rotate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The kubectl plugin&lt;/strong&gt; — &lt;code&gt;kubectl argo rollouts …&lt;/code&gt; for watching, promoting, pausing, and aborting rollouts from the terminal (and a web dashboard if you want one).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One honest nuance up front: &lt;strong&gt;precise traffic percentages need a traffic router.&lt;/strong&gt; With a plain Service, canary steps scale ReplicaSets proportionally, so weights are approximate (with 10 replicas, your steps are effectively 10% chunks). Integrate Rollouts with NGINX Ingress, ALB, Traefik, Istio, or another supported router and the weights become real traffic weights, independent of replica count. Replica-based canary is fine to learn on; traffic-based is what you want for user-facing services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install Rollouts and the kubectl plugin
&lt;/h2&gt;

&lt;p&gt;The controller:&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 argo-rollouts
kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; argo-rollouts &lt;span class="nt"&gt;-f&lt;/span&gt; https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml

&lt;span class="c"&gt;# watch it come up&lt;/span&gt;
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; argo-rollouts get pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plugin (Linux example — grab the matching binary for your OS from the same releases page):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-LO&lt;/span&gt; https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x ./kubectl-argo-rollouts-linux-amd64
&lt;span class="nb"&gt;sudo mv&lt;/span&gt; ./kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts

kubectl argo rollouts version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;latest&lt;/code&gt; in a learning cluster is fine. In production, pin the release version in the URL and manage the install through your GitOps repo like everything else — a controller that controls your releases should itself be released carefully.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your first canary: a Rollout with steps
&lt;/h2&gt;

&lt;p&gt;Take an ordinary Deployment, change &lt;code&gt;kind: Deployment&lt;/code&gt; to &lt;code&gt;kind: Rollout&lt;/code&gt; (and the apiVersion to &lt;code&gt;argoproj.io/v1alpha1&lt;/code&gt;), and add a strategy. This example rolls a new version through 20% → pause for a human look → 50% → indefinite pause → 80% → full:&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;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Rollout&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;shop-api&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;production&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;revisionHistoryLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;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;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shop-api&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;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shop-api&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;shop-api&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;ghcr.io/example/shop-api:2.4.0&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;8080&lt;/span&gt;
          &lt;span class="na"&gt;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/healthz&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;8080&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;canary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;setWeight&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pause&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5m&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;setWeight&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pause&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;          &lt;span class="c1"&gt;# waits for a human promote&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;setWeight&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pause&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;10m&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it, trigger an update by changing the image tag, and watch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl argo rollouts get rollout shop-api &lt;span class="nt"&gt;-n&lt;/span&gt; production &lt;span class="nt"&gt;--watch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will see the step list with the current step highlighted — this little tree view is, honestly, half the reason people adopt Rollouts. The state of a release stops being something you reconstruct from three commands and becomes something you can &lt;em&gt;see&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Promote past a manual pause, or bail out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl argo rollouts promote rollout shop-api &lt;span class="nt"&gt;-n&lt;/span&gt; production
kubectl argo rollouts abort rollout shop-api &lt;span class="nt"&gt;-n&lt;/span&gt; production   &lt;span class="c"&gt;# back to stable, now&lt;/span&gt;
kubectl argo rollouts undo rollout shop-api &lt;span class="nt"&gt;-n&lt;/span&gt; production     &lt;span class="c"&gt;# roll back a finished one&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Let metrics decide: analysis runs
&lt;/h2&gt;

&lt;p&gt;Manual pauses are a fine start, but they still depend on a human watching a dashboard. The feature that turns Rollouts from "nicer rolling update" into progressive delivery is the &lt;strong&gt;AnalysisRun&lt;/strong&gt;: during a canary step, Rollouts queries your metrics provider on an interval; if the numbers breach your conditions, it aborts and rolls back automatically.&lt;/p&gt;

&lt;p&gt;A minimal success-rate check against Prometheus:&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;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AnalysisTemplate&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;success-rate&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;production&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;args&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;service-name&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;success-rate&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;count&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;successCondition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;result[0] &amp;gt;= &lt;/span&gt;&lt;span class="m"&gt;0.95&lt;/span&gt;
      &lt;span class="na"&gt;failureLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;prometheus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://prometheus.monitoring.svc:9090&lt;/span&gt;
          &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;sum(rate(http_requests_total{service="{{ args.service-name }}",status!~"5.."}[5m]))&lt;/span&gt;
            &lt;span class="s"&gt;/&lt;/span&gt;
            &lt;span class="s"&gt;sum(rate(http_requests_total{service="{{ args.service-name }}"}[5m]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reference it from the Rollout's canary strategy, and the analysis starts automatically at the step you choose:&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;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;canary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;analysis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;templates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;templateName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;success-rate&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service-name&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shop-api&lt;/span&gt;
        &lt;span class="na"&gt;startingStep&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
      &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;setWeight&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pause&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5m&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;setWeight&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pause&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;10m&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the 2 a.m. scenario changes shape: the new version starts failing, the success rate dips under 95% twice, and Rollouts aborts and restores the stable ReplicaSet &lt;strong&gt;before anyone's phone buzzes&lt;/strong&gt;. The human reviews a finished, contained event in the morning instead of performing an emergency.&lt;/p&gt;

&lt;p&gt;Two disciplines make or break this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Analyse what users feel, not what machines feel.&lt;/strong&gt; Success rate and latency percentiles of real requests beat CPU graphs. (This is the same symptom-over-cause rule from my &lt;a href="https://www.devopsenginer.com/blog/kubernetes-observability-guide-2026" rel="noopener noreferrer"&gt;Kubernetes observability guide&lt;/a&gt; — analysis is only as good as the signals you feed it.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your metrics must already exist and be trustworthy.&lt;/strong&gt; If the service emits no usable request metrics, step one is observability, not Rollouts. An automated rollback wired to a meaningless query is worse than a manual one — it fails with confidence.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Blue-green with Rollouts
&lt;/h2&gt;

&lt;p&gt;Prefer an all-or-nothing switch with an instant way back? Blue-green is a strategy block away:&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;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;blueGreen&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;activeService&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shop-api-active&lt;/span&gt;
      &lt;span class="na"&gt;previewService&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shop-api-preview&lt;/span&gt;
      &lt;span class="na"&gt;autoPromotionEnabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;scaleDownDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new version comes up behind the &lt;em&gt;preview&lt;/em&gt; Service, where you can smoke-test it against real infrastructure with zero user traffic. Promote it, and the &lt;em&gt;active&lt;/em&gt; Service flips to the new ReplicaSet; the old one stays warm for &lt;code&gt;scaleDownDelaySeconds&lt;/code&gt; — your undo window. With &lt;code&gt;autoPromotionEnabled: false&lt;/code&gt;, the flip waits for &lt;code&gt;kubectl argo rollouts promote&lt;/code&gt;. Set an analysis as a &lt;code&gt;prePromotionAnalysis&lt;/code&gt; and even blue-green gets a metric gate before the switch.&lt;/p&gt;

&lt;p&gt;My rule of thumb, same as in the strategies guide: &lt;strong&gt;canary for stateless user-facing services where gradual is possible; blue-green where a version must switch atomically&lt;/strong&gt; (or where testing the exact production-bound version beforehand matters more than gradual exposure).&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollouts + Argo CD: who owns what
&lt;/h2&gt;

&lt;p&gt;If you run GitOps, the division of labour is clean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Argo CD&lt;/strong&gt; syncs the desired state from Git — including the Rollout resource and its new image tag. A release is a commit, with the audit trail Git gives you for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollouts&lt;/strong&gt; owns the &lt;em&gt;progression&lt;/em&gt; of that version inside the cluster: steps, pauses, analysis, promotion, abort.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Argo CD understands Rollouts natively — it reports the Rollout's health (Progressing / Healthy / Degraded) instead of the binary in-sync view you'd get with a Deployment mid-rollout, so a canary sitting at 20% doesn't show up as a permanently "progressing" mystery.&lt;/p&gt;

&lt;p&gt;One trap: do not let two systems fight. Your CI pipeline should update the image tag in Git (or the Rollout manifest) and then &lt;em&gt;stop&lt;/em&gt; — no &lt;code&gt;kubectl promote&lt;/code&gt; step in CI for releases you want gated, and no second controller trying to manage the same ReplicaSets. One owner per decision: Git for &lt;em&gt;what version&lt;/em&gt;, Rollouts for &lt;em&gt;how fast it earns traffic&lt;/em&gt;, humans for &lt;em&gt;the pauses you deliberately kept&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes that bite
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Canary by replicas, expectations by traffic.&lt;/strong&gt; With few replicas and no traffic router, a "20%" step can be one pod behind a Service — noisy and not really 20% of anything. If precise percentages matter to the release decision, wire up a traffic router first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analysis wired to vanity metrics.&lt;/strong&gt; An AnalysisRun checking cluster CPU will happily promote a version that is erroring on every tenth request. Gate on per-service success rate and latency, measured on the canary itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No baseline.&lt;/strong&gt; Analysis compares the canary against an absolute condition (like the 95% above). If normal for this service is 96% on a good day, a 95% gate aborts healthy releases and trains everyone to ignore aborts. Tune the gate to the service's real baseline — my &lt;a href="https://www.devopsenginer.com/blog/kubernetes-resource-requests-limits-guide-2026" rel="noopener noreferrer"&gt;requests and limits guide&lt;/a&gt; makes the same point about numbers copied from other people's clusters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promoting during an incident.&lt;/strong&gt; If the platform is already on fire, a promote command is not a fix. Rollouts will even keep an in-flight canary paused politely while everything degrades around it. Abort first; release later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting the HPA.&lt;/strong&gt; Rollouts scales the stable and canary ReplicaSets to honour weights; an HPA targeting the Rollout works (point it at the Rollout, not the ReplicaSets), but an HPA someone left aimed at the old Deployment setup will confuse everyone. One more reason the migration should be a deliberate manifest change, not a weekend experiment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;revisionHistoryLimit left huge.&lt;/strong&gt; Old ReplicaSets are your rollback targets — keep a few (2–3). Keep thirty and every &lt;code&gt;get rollout&lt;/code&gt; becomes an archaeology dig.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When NOT to use Rollouts
&lt;/h2&gt;

&lt;p&gt;Honestly: plenty of services should stay on plain Deployments.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Internal tools and low-traffic services where a bad release inconveniences three colleagues, not three thousand customers.&lt;/li&gt;
&lt;li&gt;Batch and cron workloads — there is no traffic to graduate.&lt;/li&gt;
&lt;li&gt;Anything whose metrics are too thin to analyse; a canary you cannot verify is just a slower rolling update.&lt;/li&gt;
&lt;li&gt;Teams still fighting basic stability. Progressive delivery amplifies a release process; it does not repair one. Get deployments boring first (the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-troubleshooting-guide-2026" rel="noopener noreferrer"&gt;troubleshooting guide&lt;/a&gt; is the place to start), then add graduation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The measure I use, from the project-manager side of my desk: does a bad release of this service cost more than the machinery to catch it early? For the public, revenue-adjacent, or reputation-carrying services, Rollouts pays for itself the first time it aborts at step one. For everything else, it's ceremony.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Argo Rollouts a replacement for Argo CD?&lt;/strong&gt;&lt;br&gt;
No — they are siblings, not competitors. Argo CD delivers desired state from Git to the cluster. Rollouts controls how a new version progresses once it's there. Many teams run both; Rollouts also works without Argo CD if your delivery is plain kubectl or another CD tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use Rollouts without an ingress controller or service mesh?&lt;/strong&gt;&lt;br&gt;
Yes. You get replica-proportional canary steps through Services, which is approximate but functional — fine for learning and for low-stakes services. Precise, replica-independent traffic weights need a supported traffic router (NGINX, ALB, Traefik, Istio, and others).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens to my Deployment manifests?&lt;/strong&gt;&lt;br&gt;
You convert them: &lt;code&gt;kind: Rollout&lt;/code&gt; replaces &lt;code&gt;kind: Deployment&lt;/code&gt;, the pod template carries over unchanged, and you add the &lt;code&gt;strategy&lt;/code&gt; block. Rollouts manages its own ReplicaSets from there. The conversion is small; the thinking about steps and gates is the real work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does an aborted canary count as downtime?&lt;/strong&gt;&lt;br&gt;
No — aborting shifts traffic back to the stable ReplicaSet, which never went away. Users saw the canary's behaviour only for the share of traffic it held, which is precisely the blast-radius reduction you adopted this for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can analysis roll back a blue-green release too?&lt;/strong&gt;&lt;br&gt;
Yes — pre-promotion analysis gates the switch, and post-promotion analysis can trigger a rollback to the previous ReplicaSet while it's still within its scale-down window.&lt;/p&gt;

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

&lt;p&gt;A Deployment rolls software out. Argo Rollouts rolls software out &lt;em&gt;on probation&lt;/em&gt;: a small share of traffic, real metrics watching, automatic retreat when the numbers say so. Install the controller, convert one service, start with replica-based canary steps and a manual pause — then wire in analysis and graduate to "the release promotes itself, and pages nobody."&lt;/p&gt;

&lt;p&gt;Ship the versions. Let the evidence promote them.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Keep going:&lt;/strong&gt; &lt;a href="https://www.devopsenginer.com/blog/kubernetes-deployment-strategies-2025-complete-guide" rel="noopener noreferrer"&gt;Deployment Strategies&lt;/a&gt; · &lt;a href="https://www.devopsenginer.com/blog/argo-cd-tutorial-gitops-2026" rel="noopener noreferrer"&gt;Argo CD Tutorial&lt;/a&gt; · &lt;a href="https://www.devopsenginer.com/blog/kubernetes-observability-guide-2026" rel="noopener noreferrer"&gt;Kubernetes Observability&lt;/a&gt; · &lt;a href="https://www.devopsenginer.com/#newsletter" rel="noopener noreferrer"&gt;Kubernetes Production Checklist (free PDF)&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>argocd</category>
      <category>gitops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Kubernetes Observability 2026: Metrics, Logs and Traces Without the Noise</title>
      <dc:creator>softverseAdmin</dc:creator>
      <pubDate>Sat, 31 Oct 2026 04:22:09 +0000</pubDate>
      <link>https://dev.to/softverseadmin/kubernetes-observability-2026-metrics-logs-and-traces-without-the-noise-3fmf</link>
      <guid>https://dev.to/softverseadmin/kubernetes-observability-2026-metrics-logs-and-traces-without-the-noise-3fmf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Kubernetes observability is three signals with three different jobs — metrics tell you &lt;em&gt;that&lt;/em&gt; something is wrong, logs tell you &lt;em&gt;what&lt;/em&gt; happened, traces tell you &lt;em&gt;where&lt;/em&gt; it went — plus a fourth everyone forgets: events, the cluster's own diary. The craft is not collecting more; it's discipline: golden signals per service, structured logs shipped off the pod, traces sampled honestly, strict label cardinality, and alerts that fire on symptoms users feel. Wire those in and incidents get shorter instead of dashboards getting longer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.devopsenginer.com/blog/kubernetes-observability-guide-2026" rel="noopener noreferrer"&gt;DevOps Enginer&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2 a.m. questions
&lt;/h2&gt;

&lt;p&gt;Every incident, whatever the technology, comes down to two questions: &lt;em&gt;what changed?&lt;/em&gt; and &lt;em&gt;what is it doing now?&lt;/em&gt; I learned the audit-trail half of that lesson teaching myself AWS on my own account — my habit was to open CloudTrail and read what had actually been created and changed, instead of guessing from memory. Clusters deserve the same respect: the systems that answer those two questions in minutes are called "observable," and the ones that answer in hours are called "down."&lt;/p&gt;

&lt;p&gt;Kubernetes makes this both easier and harder than the VM world. Easier, because everything is an API object with a state and a history. Harder, because the things you used to SSH into — containers, pods — are ephemeral by design: the pod that errored at 02:00 has been rescheduled twice by 02:15, and its logs went with it. Observability in Kubernetes is therefore not a monitoring add-on; it is the deliberate act of exporting state out of things that are designed to disappear.&lt;/p&gt;

&lt;p&gt;This guide is the working model: what to collect, what each signal is actually for, and the disciplines that keep the signal bill — and the alert noise — from eating the team.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three signals, and the jobs they're for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metrics&lt;/strong&gt; — numbers over time (request rate, error rate, latency percentiles, CPU saturation). Cheap to store, fast to query, perfect for alerting and dashboards. Useless for "why" — a metric tells you the error rate tripled, never which error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs&lt;/strong&gt; — timestamped event records. The detail layer: stack traces, request IDs, the specific "what happened." Expensive at volume, worthless unstructured — a log you can't query during an incident is a diary nobody reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traces&lt;/strong&gt; — the path of one request across services, as timed spans. The only signal that answers "which hop in this chain is slow" without an afternoon of log archaeology. Costs you instrumentation effort, repaid on the first cross-service incident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the unofficial fourth: &lt;strong&gt;events&lt;/strong&gt; — the cluster's own record of decisions (scheduled, pulled, started, OOMKilled, failed probes). &lt;code&gt;kubectl get events&lt;/code&gt; is the first stop in the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-troubleshooting-guide-2026" rel="noopener noreferrer"&gt;troubleshooting workflow&lt;/a&gt; for a reason. Know its limit: events are retained for roughly an hour by default, so they are a live-incident tool, not an archive — anything you need historically must be exported.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metrics: golden signals, ruthless cardinality
&lt;/h2&gt;

&lt;p&gt;The Prometheus model won this layer: applications and exporters expose metrics, Prometheus scrapes and stores them, Grafana draws them. Managed equivalents exist on every cloud; the concepts transfer. Two disciplines make or break it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-service golden signals.&lt;/strong&gt; For every service users touch, four numbers on one row: &lt;strong&gt;latency&lt;/strong&gt; (with percentiles, not averages — the average hides the suffering), &lt;strong&gt;traffic&lt;/strong&gt; (requests per second), &lt;strong&gt;errors&lt;/strong&gt; (rate and ratio), &lt;strong&gt;saturation&lt;/strong&gt; (how full the service is: CPU vs. request, memory headroom, queue depth). This RED/USE-style framing is what turns 400 dashboards into one glance: a service is healthy or one of its four numbers is red. For the cluster itself, the same idea one level down: node saturation, pending pods, and requests-vs-usage — the last of which doubles as your cost signal (see the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-cost-optimization-guide-2026" rel="noopener noreferrer"&gt;cost optimization guide&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cardinality is the budget.&lt;/strong&gt; Every unique label combination is a separate time series, and series are what you pay for — in memory, storage, and query speed. Labels are for dimensions with few values (service, namespace, status code class). They are never for user IDs, request IDs, full URLs, or error message text — those belong in logs and traces. The classic self-inflicted wound is a &lt;code&gt;path&lt;/code&gt; label with unbounded values quietly multiplying a metric into a million series. Cardinality discipline is to metrics what requests discipline is to cost: invisible when done right, an incident when ignored.&lt;/p&gt;

&lt;p&gt;Alert on the golden signals, recorded at sane resolution, and let recording rules pre-compute the expensive queries your dashboards and alerts run every minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logs: structured, shipped, and correlated
&lt;/h2&gt;

&lt;p&gt;Three rules convert logs from storage expense into incident fuel:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Structured or it didn't happen.&lt;/strong&gt; JSON (or equivalent key-value) logs with level, service, and a correlation/request ID in every line. The request ID is the thread that ties a log line to a trace to a user complaint — propagate it at ingress and through every downstream call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Off the pod, always.&lt;/strong&gt; &lt;code&gt;kubectl logs&lt;/code&gt; reads the current container on the current node; a rescheduled pod takes its history with it — the checklist version of this rule is blunt: pod logs die with the pod. Ship to a central store (Loki, Elasticsearch/OpenSearch, a cloud logging service) with retention tiers: hot and searchable for days, archived cheap for the compliance horizon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log decisions and failures, not chatter.&lt;/strong&gt; A line per request at INFO is a billing strategy for your logging vendor. Log the boundaries (request received/completed at DEBUG or sampled), the anomalies, and the state changes — and make error logs carry the context a stranger needs at 2 a.m.: what was attempted, with what key inputs, and what the system decided.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Traces: OpenTelemetry and honest sampling
&lt;/h2&gt;

&lt;p&gt;Distributed tracing's centre of gravity is now &lt;strong&gt;OpenTelemetry&lt;/strong&gt;: one instrumentation standard, vendor-neutral, with auto-instrumentation for common frameworks and manual spans where the story needs them. The practical shape in a cluster:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instrument the edges first — ingress to service, service to service, service to database. A trace that covers the hops covers the argument.&lt;/li&gt;
&lt;li&gt;Propagate context properly (W3C traceparent headers) — a broken chain is worse than none, because it looks like data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sample honestly.&lt;/strong&gt; Head-based sampling at a modest rate for normal traffic, tail-based sampling (keep the interesting ones) where your collector supports it, and one sacred rule: errors and slow requests are always kept. Tracing everything is a storage bill; tracing nothing is a hope; tracing the failures is the point.&lt;/li&gt;
&lt;li&gt;Connect the signals: exemplars and trace IDs in your metrics and logs so an alert can jump to the metric, the metric to example traces, the trace to the exact log lines. Observability pays out at the joins.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Alerting: symptoms, budgets, and the noise floor
&lt;/h2&gt;

&lt;p&gt;The fastest way to destroy an observability practice is alerting on causes ("CPU &amp;gt; 80%") until the team mutes the channel. The working rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Page on symptoms users feel&lt;/strong&gt;: error ratio burning, latency SLO at risk, saturation heading for the cliff. Causes belong on dashboards and in tickets, where they can be investigated at human speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every page carries a runbook link&lt;/strong&gt; and answers "what do I do now?" An alert without a next action is a criticism, not a tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An alert budget is real.&lt;/strong&gt; Track pages per week the way you track cost. When the budget is blown, the work is alert hygiene — deleting, demoting, or fixing the flapping few — before any new feature work. This is also the honest metric of your &lt;a href="https://www.devopsenginer.com/blog/self-healing-infrastructure-2025" rel="noopener noreferrer"&gt;self-healing&lt;/a&gt; setup: the best alert is the one the system resolved before a human was involved, and said so quietly in the morning report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-window burn-rate alerts&lt;/strong&gt; (fast burn pages, slow burn tickets) beat fixed thresholds for SLOs, because they measure the thing the business actually promised.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Dashboards: three that earn their tabs
&lt;/h2&gt;

&lt;p&gt;Dashboard sprawl is cardinality's visual cousin. The set that survives contact with real operations is small:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Service overview&lt;/strong&gt; — the four golden signals for one service, with deploy markers overlaid. The deploy overlay is the highest-value line on any dashboard: most incidents correlate with a change, and "what changed?" should be answerable by looking, not archaeology.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster capacity&lt;/strong&gt; — node saturation, pending pods, requests-vs-usage by namespace, and the top offenders list. Operations and cost share this view, which is as it should be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The incident view&lt;/strong&gt; — one screen the on-call actually opens at 2 a.m.: golden signals for the affected service, its dependencies' health, recent events, and links into logs and traces pre-filtered to the service. Build it during a calm week; you will not build it during an incident.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else is a report, a debugging scratchpad, or nostalgia. Archive aggressively; a dashboard nobody opens in a quarter is a liability with a URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability as code
&lt;/h2&gt;

&lt;p&gt;Dashboards and alerts defined by clicking are configuration drift with a login page. The GitOps answer applies here exactly as it does to workloads: dashboards as JSON in git, alert rules as PrometheusRule objects or files in the repo, recording rules reviewed in pull requests. The benefits compound: changes are reviewed (a threshold change is a policy change), history is blame-able in the good sense, and a rebuilt monitoring stack comes back identical — our &lt;a href="https://www.devopsenginer.com/blog/argo-cd-tutorial-gitops-2026" rel="noopener noreferrer"&gt;Argo CD tutorial&lt;/a&gt; pattern works for the observability stack itself, which can live in the same app-of-apps layout as everything else you run.&lt;/p&gt;

&lt;h2&gt;
  
  
  What observability costs (and how it balloons)
&lt;/h2&gt;

&lt;p&gt;Observability spend surprises teams the way cluster spend does: through accumulation, not decisions. The three leaks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metric cardinality&lt;/strong&gt; — the unbounded label, priced per series (see above).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log volume&lt;/strong&gt; — the chatty service at INFO, priced per gigabyte, forever. Sample, tier, and give every team's its own volume number — showback works here exactly as it does for compute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trace retention&lt;/strong&gt; — full-fidelity tracing retained for months, priced per span. Sample the boring, keep the broken, archive the rest cold.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put the observability bill on the same monthly one-pager as the cluster bill. A monitoring stack that costs a visible fraction of the infrastructure it watches is normal; an &lt;em&gt;unexamined&lt;/em&gt; one grows until finance asks the question in a tone nobody enjoys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes that make observability useless
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Collecting everything, querying nothing.&lt;/strong&gt; Data without a dashboard, alert, or runbook attached is storage, not observability. Every signal should name the decision it feeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Averages for latency.&lt;/strong&gt; The mean of a bimodal distribution describes nobody's experience. Percentiles (p50/p95/p99) or it didn't happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unbounded labels.&lt;/strong&gt; One &lt;code&gt;user_id&lt;/code&gt; label can take down the metrics store that was supposed to tell you about the outage — during the outage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs only on the pod.&lt;/strong&gt; The first rescheduling during an incident deletes the evidence. Shipping is not optional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerting on causes until nobody listens.&lt;/strong&gt; A muted channel is worse than no channel: it looks like coverage. Symptom-based paging, budgeted and reviewed, or expect the 2 a.m. page to be the one everyone sleeps through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability configured by clicking.&lt;/strong&gt; Unreviewed thresholds and dashboards that vanish when the stack is rebuilt. If it matters, it lives in git.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prometheus/Grafana, or my cloud's managed monitoring?&lt;/strong&gt;&lt;br&gt;
Start with whichever your team will actually open — a managed stack beats an unmaintained Prometheus. The concepts in this guide (golden signals, cardinality, symptom alerts) are portable across both; move when scale, multi-cluster needs, or cost make the case, not before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need traces if I have good logs?&lt;/strong&gt;&lt;br&gt;
Logs tell you what each service did; traces tell you how the request's time and errors distribute &lt;em&gt;across&lt;/em&gt; services. With two services, logs plus correlation IDs may carry you. Past a handful of hops, the cross-service incident without traces is an afternoon of archaeology — that's the moment tracing pays for its instrumentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What should retention be?&lt;/strong&gt;&lt;br&gt;
Tier it by use: metrics at full resolution for weeks, downsampled for a year or more (capacity questions are seasonal); logs hot and searchable for days to weeks, archived to cheap storage for your compliance horizon; traces sampled with short retention — their value is in the incident week, not the archive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I observe the observability stack itself?&lt;/strong&gt;&lt;br&gt;
Meta, but necessary: scrape the scrapers, alert on Prometheus/Loki/collector health from a dead-simple external check (even an uptime ping on the alerting path), and rehearse the "monitoring is down" case once — the worst incidents are the ones you discover from users because the dashboards were the thing that broke.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where does this fit the production checklist?&lt;/strong&gt;&lt;br&gt;
Section 5 of the &lt;a href="https://www.devopsenginer.com/" rel="noopener noreferrer"&gt;Kubernetes Production Checklist&lt;/a&gt; — the four signals per service, logs shipped off-pod, alerts with runbooks, and the one-command answers to "what changed / what's running / what's it costing" — is this guide compressed into checkboxes. (The full checklist PDF is the free lead magnet on the DevOps Enginer homepage.)&lt;/p&gt;

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

&lt;p&gt;Observability is not a stack you install; it's the property that your two 2 a.m. questions — what changed, and what is it doing now — have fast, trustworthy answers. Metrics for the &lt;em&gt;that&lt;/em&gt;, logs for the &lt;em&gt;what&lt;/em&gt;, traces for the &lt;em&gt;where&lt;/em&gt;, events for the cluster's own account of itself, all of it disciplined by cardinality, structure, honest sampling, and alerts that respect the humans they wake. Build the three dashboards, wire the deploy markers, put it all in git, and review the alert budget like a bill — because it is one. Your future on-call self, mid-incident, is the customer. Build for them.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>observability</category>
      <category>prometheus</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Platform Engineering in Practice 2026: Golden Paths, Portals, and Proof Anyone Uses Them</title>
      <dc:creator>softverseAdmin</dc:creator>
      <pubDate>Sat, 31 Oct 2026 03:49:53 +0000</pubDate>
      <link>https://dev.to/softverseadmin/platform-engineering-in-practice-2026-golden-paths-portals-and-proof-anyone-uses-them-26k3</link>
      <guid>https://dev.to/softverseadmin/platform-engineering-in-practice-2026-golden-paths-portals-and-proof-anyone-uses-them-26k3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Platform engineering succeeds or fails on one question — will anyone actually use it? In practice that means: build golden paths (opinionated, end-to-end routes from idea to production) instead of handing out raw infrastructure, treat the portal as the shop window rather than the shop, run the platform as a product with internal customers who can say no, and measure adoption and lead time instead of counting what you built. Start with one thinnest viable platform for one real team, and let demand — not a roadmap deck — pull the next feature.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.devopsenginer.com/blog/platform-engineering-in-practice-2026" rel="noopener noreferrer"&gt;DevOps Enginer&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The question that decides everything
&lt;/h2&gt;

&lt;p&gt;When I moved from DevOps engineering into project management, my core question changed. As an engineer it was "is it built right?" As a PM it became "will anyone actually use it?" I didn't expect that second question to be a platform engineering question — but it is the platform engineering question, asked verbatim.&lt;/p&gt;

&lt;p&gt;Every failed internal platform I've seen described (and every post-mortem the community shares) fails the same way: a talented infrastructure team builds something technically excellent — a beautiful self-service portal, a pristine Kubernetes estate, immaculate Terraform modules — and the product teams keep opening tickets, running their own shadow pipelines, or quietly deploying around it. The platform was built right. Nobody used it. Our earlier piece on &lt;a href="https://www.devopsenginer.com/blog/rise-of-platform-engineering-redefining-devops-2025" rel="noopener noreferrer"&gt;the rise of platform engineering&lt;/a&gt; covered why the movement exists; this one is about the operating discipline that makes it work in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a golden path actually is
&lt;/h2&gt;

&lt;p&gt;"Golden path" and "paved road" get used loosely, so let's be concrete. A golden path is an &lt;strong&gt;opinionated, supported, end-to-end route&lt;/strong&gt; for one common job — the canonical example being "a new backend service, production-ready":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A service template/scaffold: repo created with the standard layout, CODEOWNERS, and a README that isn't empty.&lt;/li&gt;
&lt;li&gt;CI already wired: build, test, image scan, deploy stages — the &lt;a href="https://www.devopsenginer.com/blog/github-actions-reusable-workflows-guide-2026" rel="noopener noreferrer"&gt;reusable-workflow pattern&lt;/a&gt;, not a blank workflow file and good wishes.&lt;/li&gt;
&lt;li&gt;Delivery already wired: a GitOps-managed deployment (in our world, Argo CD — see the &lt;a href="https://www.devopsenginer.com/blog/argo-cd-tutorial-gitops-2026" rel="noopener noreferrer"&gt;tutorial&lt;/a&gt;) into the standard namespaces, with requests/limits defaults from the &lt;a href="https://www.devopsenginer.com/blog/kubernetes-resource-requests-limits-guide-2026" rel="noopener noreferrer"&gt;requests and limits guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The un-fun parts included: TLS, ingress, dashboards, alert routes, a runbook stub, cost labels. The things every team needs and no team wants to rediscover.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two properties make it a &lt;em&gt;path&lt;/em&gt; rather than a template dump. First, &lt;strong&gt;it's supported&lt;/strong&gt;: a named team owns it, fixes it, and answers questions about it. Second, &lt;strong&gt;it's optional in principle&lt;/strong&gt;: teams may leave the path — and own everything the path would have given them. That combination (attractive enough to choose, honest about the trade) is what separates a golden path from a mandate wearing a friendlier name. Mandates produce compliance theatre; paths produce adoption metrics you can believe.&lt;/p&gt;

&lt;h2&gt;
  
  
  The portal is the shop window, not the shop
&lt;/h2&gt;

&lt;p&gt;Backstage is the name everyone knows, and developer portals absolutely have their place: a service catalog, one searchable home for docs and ownership, a front door for scaffolding. But the portal-first project is the most common platform failure of the last few years. The pattern: six months building the portal, a launch announcement, and then the discovery that the underlying capabilities — provisioning, deployment, secrets, environments — are still ticket queues wearing a web form.&lt;/p&gt;

&lt;p&gt;The working order is the reverse:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Capabilities first.&lt;/strong&gt; Can a team actually create a compliant environment, deploy, and get credentials without a human in the middle? If not, no interface fixes that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interfaces second.&lt;/strong&gt; CLI, GitOps repo, API, portal — in whatever order your users actually pull. Many healthy platforms run for years on templates + docs + a catalog page before a full portal earns its keep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The portal when it pays.&lt;/strong&gt; When discoverability becomes the bottleneck — hundreds of services, new hires drowning — the portal consolidates what already works. It amplifies a platform; it cannot substitute for one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A useful test: if your portal went down for a day, would teams be inconvenienced or blocked? Inconvenienced means the platform is real and the portal is its window. Blocked means you built a very expensive single point of failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it as a product, because it is one
&lt;/h2&gt;

&lt;p&gt;The Team Topologies framing popularised the idea of the &lt;strong&gt;Thinnest Viable Platform&lt;/strong&gt;: the smallest platform that meaningfully accelerates the stream-aligned teams — and no bigger. The discipline behind the phrase is pure product management:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Internal customers, real interviews.&lt;/strong&gt; Before building, sit with two or three product teams. Where do they actually lose time — provisioning, flaky environments, deploy approvals, observability setup? Rank by their pain, not by your curiosity. (This is the PM question again: the backlog is theirs, not yours.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A roadmap measured in outcomes.&lt;/strong&gt; "Ship Backstage" is an output. "New service to production in under a day, without a ticket" is an outcome. Roadmaps written in outcomes survive contact with reality; output roadmaps produce demo-ware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adoption is the metric that can't be gamed.&lt;/strong&gt; Percentage of services on the golden path, lead time from first commit to production for path users vs. off-path, ticket volume for things the platform now self-serves, and the quietest, most honest signal of all: do teams recommend it to each other? Anything you count, also ask whether the number would move if the platform got &lt;em&gt;worse&lt;/em&gt; — if not, it's a vanity metric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support is a feature.&lt;/strong&gt; Office hours, a Slack channel with a response norm, docs written for the reader's deadline rather than the author's completeness. Platforms die of unanswered questions more often than of missing features.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Start thin: your first golden path in about a month
&lt;/h2&gt;

&lt;p&gt;For a team starting from zero, a realistic first iteration looks like this — scoped for one pilot team with one common service shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Week 1 — pick the slice with the pilot team.&lt;/strong&gt; Interview them, watch their current path to production, and write down every step that is waiting, copying, or asking. Choose the one service archetype they ship most (usually a stateless HTTP service) and define "done": scaffold → merged → deployed to production, no tickets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 2 — build the scaffold.&lt;/strong&gt; Repo template with CI wired (reusable workflows), a Helm chart or kustomize base with sane defaults, Argo CD application manifest, dashboard and alert stubs. Ugly is fine; manual steps inside the template's README get listed as known debt, not hidden.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 3 — the pilot team ships something real on it.&lt;/strong&gt; Sit with them (pair, don't present). Every friction point they hit is your backlog, in their priority order. Expect the path to be wrong in at least three places — that's the pilot working as intended.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 4 — harden the top frictions, write the docs from the pilot's notes, and invite team two.&lt;/strong&gt; Not an org-wide launch: a second volunteer team. Volunteers are the adoption metric that predicts the rest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Resist two expansions during this month: a portal (see above) and a second archetype. Depth on one path beats breadth across five half-paths, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Escape hatches are a feature
&lt;/h2&gt;

&lt;p&gt;A golden path that fits 100% of cases is a fantasy; one that fits 80% and handles the other 20% gracefully is a platform. Design the off-ramps deliberately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documented deviations.&lt;/strong&gt; "Need a non-standard database? Here's the pattern, here's what you now own (backups, upgrades, on-call), here's who to tell." The cost of leaving the path should be explicit and paid consciously, not discovered during an audit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extension points in the templates.&lt;/strong&gt; Values files, overlays, and workflow inputs that let teams tune the common knobs without forking the whole scaffold. Forks are where platform improvements go to die — every fork is a team that stops receiving your fixes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A feedback loop with teeth.&lt;/strong&gt; Deviations are product research: if five teams all deviate the same way, the path is wrong, not the teams. Review deviation patterns quarterly and promote the common ones into the path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also where the cognitive-load argument lands honestly: the goal was never to remove all infrastructure thinking from product teams — it's to remove the &lt;em&gt;undifferentiated&lt;/em&gt; infrastructure thinking, so their attention goes to the parts that are genuinely theirs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI fits (and where it doesn't)
&lt;/h2&gt;

&lt;p&gt;AI tooling slots into a platform the same way it slots into DevOps generally — as an accelerator on the path, not a replacement for it (the full picture is in our &lt;a href="https://www.devopsenginer.com/blog/ai-for-devops-practical-guide-2026" rel="noopener noreferrer"&gt;AI for DevOps guide&lt;/a&gt;). Practical fits: generating first drafts of runbooks and docs from the templates, summarising incidents for the platform's users, and helping scaffold consumers customise the last 10% of a template. The trap is an "AI portal" that generates arbitrary infrastructure — it produces the opposite of a golden path: a thousand unique snowflakes, each confidently generated and none of them supported. The path stays opinionated; AI helps people walk it faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes I've learned to recognise
&lt;/h2&gt;

&lt;p&gt;Platform teams rarely fail on technology. The recurring shapes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The ticket team with a new name.&lt;/strong&gt; The "platform team" still spends its weeks processing provisioning tickets by hand. If the work didn't become self-service, the rebrand was the deliverable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ivory-tower architecture.&lt;/strong&gt; Standards designed without the pilot-team loop, announced as mandates, measured by compliance instead of adoption. Teams route around them within a quarter — usually silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building for the demo.&lt;/strong&gt; Optimising the launch presentation (portal screenshots, feature lists) instead of the second team's onboarding. Demos end; onboarding is forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform as a cost-cutting weapon.&lt;/strong&gt; The moment teams believe the platform exists to justify headcount reduction, trust — the actual currency of adoption — is spent. Platforms are leverage, and leverage arguments only work when the lever's users believe it too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No migration story.&lt;/strong&gt; A great new path that requires teams to re-platform in a flag week will be adopted in never. Provide the incremental route: new services on the path now, existing services adopt piece by piece (CI first, delivery second, observability third).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Mistakes that quietly kill platforms
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Counting what you built instead of who uses it.&lt;/strong&gt; Launch metrics belong to marketing. Adoption, lead time, and ticket deflection belong to platforms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating docs as the last step.&lt;/strong&gt; Undocumented self-service is a rumour. The docs are part of the path — write them from the pilot's notes, in week 4, not "when things settle."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting the off-path teams.&lt;/strong&gt; The 20% with legitimate special cases are your harshest critics or your best case studies. Support their deviations visibly and they defend the platform in rooms you're not in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Letting the platform team's own on-call rot.&lt;/strong&gt; If the path breaks and nobody is paged, every team learns the same lesson: the path is not really supported. Reliability of the platform is the platform's first SLO.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling the team before the demand.&lt;/strong&gt; A three-person platform team serving five teams can be exactly right. Growing the platform org ahead of adoption just raises the ROI bar the platform must clear to survive its next budget review.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How big should a platform team be?&lt;/strong&gt;&lt;br&gt;
As small as possible while owning the path end-to-end — often two to four engineers for the first golden paths. The binding constraint is scope, not headcount: one well-supported archetype per small team beats a large team spread across a portal, three archetypes, and a migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backstage or build our own portal?&lt;/strong&gt;&lt;br&gt;
Start from the test in this article: capabilities first. If provisioning and deployment are already self-service and discoverability is the pain, Backstage (or a commercial IDP) consolidates them well. If the capabilities are still tickets, a portal of any origin is a prettier queue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we convince leadership it's working?&lt;/strong&gt;&lt;br&gt;
Report the product metrics: adoption share, lead-time change for path users, self-service rate for the top former ticket types, and team satisfaction from a two-question quarterly survey. "We launched features" convinces nobody twice; "a new service reaches production in a day and most teams choose the path" is a renewal argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platform engineering vs. DevOps vs. SRE — who owns what?&lt;/strong&gt;&lt;br&gt;
In practice: the platform team owns the paved road (templates, self-service capabilities, the catalog), SRE owns reliability outcomes and the practices around them, and "DevOps" is the culture both operate in — product teams owning what they run, with the platform making that ownership cheap. Titles vary; the seams above are the ones that work. Our &lt;a href="https://www.devopsenginer.com/blog/devsecops-checklist-securing-pipeline-2025" rel="noopener noreferrer"&gt;DevSecOps checklist&lt;/a&gt; shows how the security layer weaves through the same path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can a small company do platform engineering?&lt;/strong&gt;&lt;br&gt;
Yes — at small scale the "platform" might be one engineer's 20% time maintaining templates and a deploy pipeline for five teams. The product discipline (thin, adopted, supported) matters more than the org chart. What small companies can't afford is the big-bang version: a dedicated team building a portal nobody asked for.&lt;/p&gt;

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

&lt;p&gt;Platform engineering in practice is product management applied to internal infrastructure. The technology — Kubernetes, GitOps, portals, templates — is the visible tenth. The other nine-tenths are the PM questions: Who is this for? What pain does it remove? Will anyone actually use it — and how will we know? Build one thin, supported, genuinely optional golden path for one real team; measure adoption like your budget depends on it, because it does; and let the teams who walk the path pull its next features into existence. That's the whole discipline — the rest is implementation detail, and the &lt;a href="https://www.devopsenginer.com/blog/cicd-monorepos-guide-2026" rel="noopener noreferrer"&gt;monorepo CI guide&lt;/a&gt; is a good example of what one of those details looks like when it's done as a platform would do it.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>platformengineering</category>
      <category>kubernetes</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>CI/CD for Monorepos 2026: Build Only What Changed</title>
      <dc:creator>softverseAdmin</dc:creator>
      <pubDate>Sat, 31 Oct 2026 03:30:49 +0000</pubDate>
      <link>https://dev.to/softverseadmin/cicd-for-monorepos-2026-build-only-what-changed-144</link>
      <guid>https://dev.to/softverseadmin/cicd-for-monorepos-2026-build-only-what-changed-144</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; A monorepo with naive CI rebuilds the world on every commit, so pipelines get slower exactly as the repo succeeds. The fix is affected-only delivery: detect what changed, expand through the dependency graph, build and test only that slice, lean on remote caching for the rest, and protect the main branch with required checks plus a merge queue. Start with path filters in GitHub Actions; graduate to a build tool (Nx, Turborepo, Bazel) when the graph outgrows what filters can express.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.devopsenginer.com/blog/cicd-monorepos-guide-2026" rel="noopener noreferrer"&gt;DevOps Enginer&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One repo, everything inside
&lt;/h2&gt;

&lt;p&gt;My own website lives in a single repository: the Next.js blog, the API routes, shared components — and, sitting in a folder of its own, the exported files of a 3D portfolio game I built. It's a small repo by industry standards, and it already shows the monorepo shape: unrelated things sharing one history, one issue tracker, one pipeline.&lt;/p&gt;

&lt;p&gt;That shape is why teams choose monorepos in the first place. Shared code is actually shared — no version-pinning dance between repos. A refactor can move an API and all its callers in one atomic commit. One CI system to maintain instead of twelve. The costs arrive later, and they arrive in the pipeline: when every commit triggers every build, every test suite, and every deploy, the feedback loop that made the monorepo attractive quietly dies. A one-line change to a README queues behind a full rebuild of services it never touched.&lt;/p&gt;

&lt;p&gt;This guide is the delivery design that keeps the benefits: CI that does work proportional to the change, not proportional to the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why naive CI breaks as the repo grows
&lt;/h2&gt;

&lt;p&gt;Three failure modes, always in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Time grows linearly with the repo, not the change.&lt;/strong&gt; Ten services means ten builds per pull request — including the nine you didn't touch. At fifty services the pipeline is an hour, and engineers start batching changes to "save runs," which makes every run bigger and riskier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flakiness becomes everyone's blocker.&lt;/strong&gt; One unreliable test in a service you don't own turns your PR red. Multiply by every suite in the repo and a permanently-red main branch stops being an exception.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploys couple things the repo was supposed to decouple.&lt;/strong&gt; If the pipeline ships everything on every merge, the monorepo has the blast radius of a monolith with the operational overhead of microservices — the worst trade on the table.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these are arguments against monorepos. They're arguments against running a monorepo like it's fifty polyrepos sharing a trench coat.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: affected-only builds
&lt;/h2&gt;

&lt;p&gt;Every working monorepo pipeline implements the same four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detect&lt;/strong&gt; which files changed in this PR or push.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map&lt;/strong&gt; files to the projects (packages, services, apps) that own them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expand&lt;/strong&gt; through the dependency graph: a change to a shared library affects every project that depends on it — transitively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute&lt;/strong&gt; builds, tests, and deploys for the affected set only. Everything else is skipped, with its previous green result standing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 3 is the one teams skip, and it's the one that matters. Path filters alone answer "what changed"; correctness needs "what is affected." A change inside &lt;code&gt;packages/ui&lt;/code&gt; that doesn't rebuild the three apps importing it hasn't saved you time — it has deferred a breakage to someone else's deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 1: path filters in GitHub Actions
&lt;/h2&gt;

&lt;p&gt;For many teams, GitHub Actions' built-in path filtering plus a change-detection action covers step 1–2 honestly, and that's enough for a long time. Path filters on the trigger:&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&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;services/api/**'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;packages/**'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.github/workflows/api.yml'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And for finer control — one workflow that fans out per project — a changes job whose outputs gate the rest:&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;changes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.filter.outputs.api }}&lt;/span&gt;
      &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.filter.outputs.web }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dorny/paths-filter@v3&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;filter&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;api:&lt;/span&gt;
              &lt;span class="s"&gt;- 'services/api/**'&lt;/span&gt;
              &lt;span class="s"&gt;- 'packages/shared/**'&lt;/span&gt;
            &lt;span class="s"&gt;web:&lt;/span&gt;
              &lt;span class="s"&gt;- 'apps/web/**'&lt;/span&gt;
              &lt;span class="s"&gt;- 'packages/shared/**'&lt;/span&gt;

  &lt;span class="na"&gt;build-api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;changes&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.changes.outputs.api == 'true'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "build and test the API"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;packages/shared/**&lt;/code&gt; appears in both filters — a hand-maintained shadow of the dependency graph. That's the honest limit of this level: fine for a handful of projects with shallow sharing, brittle once the graph gets deep. In my own setup, personal projects live on GitHub and deploy through Vercel on push to main, and Vercel's own monorepo handling (root directory settings, ignored build steps) plays this same role for the deploy side — skip the build when the project's slice didn't change. The principle is identical at every scale: pay only for what the change touches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 2: let a build tool own the graph
&lt;/h2&gt;

&lt;p&gt;When hand-written filters start lying — shared packages nested inside shared packages, generated code, mixed languages — the graph needs a real owner. The well-known names, by ecosystem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nx&lt;/strong&gt; — JavaScript/TypeScript-first, with &lt;code&gt;nx affected&lt;/code&gt; computing the affected set from its project graph, plus task caching locally and remotely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turborepo&lt;/strong&gt; — lighter-weight, pipeline-oriented, popular with Next.js-style app collections; remote cache is its headline feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bazel&lt;/strong&gt; — the industrial option: language-agnostic, hermetic builds, precise dependency graphs; the power is real and so is the adoption cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pants / Please / moon&lt;/strong&gt; — worth knowing exist if your repo is Python-heavy, polyglot, or JS with different tastes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The evaluation question is never "which is best" — it's "who maintains our graph, and what does the cache hit rate look like." Whichever you choose, the affected computation and the cache are the product; the CLI is packaging. I learned the mirror of this lesson with Jenkins years ago: every capability arrives as a plugin you then own, so a tool's maintenance tail is part of its price. Monorepo tooling is no different — adopt the smallest tool whose graph you trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching: the second half of "only build what changed"
&lt;/h2&gt;

&lt;p&gt;Affected-only execution shrinks the work; caching shrinks what's left. Two layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dependency caching&lt;/strong&gt; — the package manager's store (npm/pnpm, pip, Maven, Go modules) keyed by lockfile hash, via &lt;code&gt;actions/cache&lt;/code&gt; or your tool's built-in handling. This is table stakes; a monorepo without it re-downloads the internet on every run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task/build caching&lt;/strong&gt; — the monorepo tool records inputs (source files, dependency outputs, environment, command) and replays stored outputs when inputs repeat. Locally it makes the second &lt;code&gt;build&lt;/code&gt; instant; &lt;strong&gt;remote caching&lt;/strong&gt; shares those results across the team and CI, so a branch whose code matches something a teammate already built skips the work entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The discipline that makes caching safe is hermetic inputs: if a build secretly reads the current time, an unpinned network fetch, or a file outside its declared inputs, the cache will eventually serve you a confidently wrong artifact. Declare inputs honestly, pin versions in lockfiles, and treat a suspicious cache result as a build bug, not as weather.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing strategy: fast where it's local, honest where it's shared
&lt;/h2&gt;

&lt;p&gt;Running "the affected tests" has the same graph trap as building: a change to a shared library should run its dependents' tests too. Practical shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit tests&lt;/strong&gt; run for the affected set on every PR — these must be fast and hermetic, or nothing else here works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration/contract tests&lt;/strong&gt; for the affected set plus its direct dependents. Shared libraries earn extra scrutiny: their blast radius is the argument for their existence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End-to-end tests&lt;/strong&gt; are the budget item. Run a smoke subset on PRs, the full suite on merge to main (or nightly), and keep them owned — an E2E test with no owner becomes a flake generator with a fan club.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quarantine flaky tests deliberately.&lt;/strong&gt; A flaky test in a monorepo taxes every PR in the repo. The honest pattern: automatic detection (rerun-to-pass patterns flagged), a quarantine list with an owner and an expiry date, and the test still running in a non-blocking job so the signal isn't lost. Pretending flakiness is a discipline problem instead of an engineering one is how main branches stay red.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Per-project pipeline definitions also don't have to mean copy-pasted YAML: &lt;a href="https://www.devopsenginer.com/blog/github-actions-reusable-workflows-guide-2026" rel="noopener noreferrer"&gt;reusable workflows and composite actions&lt;/a&gt; let one maintained definition serve every project in the repo, with inputs for what differs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Releases and versioning inside one repo
&lt;/h2&gt;

&lt;p&gt;A monorepo doesn't imply a single version or a single release train. Two common models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Independent versioning&lt;/strong&gt; — each package/service versions and releases on its own (libraries published per-package, services deployed per affected set). Fits services; needs tooling discipline (changeset-style workflows for JS repos are the well-known pattern) so humans state the bump intent with the change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixed/lockstep versioning&lt;/strong&gt; — everything versions together. Simpler mental model, fits tightly-coupled product suites and app monorepos where "the version" means the whole product.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deploys follow the same affected logic as builds: a merge to main deploys the affected services, each through its own pipeline — and if you deliver with GitOps, each affected service's desired state updates in git independently, exactly the model in our &lt;a href="https://www.devopsenginer.com/blog/gitops-vs-traditional-cicd-2025-comparison" rel="noopener noreferrer"&gt;GitOps vs traditional CI/CD comparison&lt;/a&gt;. The anti-pattern to refuse: a "deploy everything Friday" train created because per-service deploys were never made safe. That train is a monolith wearing a monorepo's clothes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protecting main: required checks and merge queues
&lt;/h2&gt;

&lt;p&gt;Affected-only CI creates one subtle hazard: PR checks and main-branch checks can test different sets. A PR that skips a job because its slice didn't change can merge into a main where that job would have failed — two individually-green PRs can compose into a red main.&lt;/p&gt;

&lt;p&gt;The mechanisms that close the hole:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Required status checks&lt;/strong&gt; per project path, so a PR can't merge without its affected jobs passing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A merge queue&lt;/strong&gt; (GitHub's is built in) that tests the &lt;em&gt;merged result&lt;/em&gt; — your PR combined with everything ahead of it in the queue — before it lands. This is the piece that makes "main is always green" a property of the system instead of a hope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A scheduled full-repo run&lt;/strong&gt; (nightly) as the safety net: everything builds, everything tests, and failures are triaged as platform work in the morning — not discovered by the next unrelated PR.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When a monorepo is the wrong answer
&lt;/h2&gt;

&lt;p&gt;Honesty section, because the migration runs both ways. Split (or don't adopt) when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Access control genuinely differs&lt;/strong&gt; — regulated or client-confidential code that must not be visible to the whole engineering org doesn't belong in the shared repo. (This is a real constraint, not an aesthetic one; it decides the question on its own.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release cadences are hostile&lt;/strong&gt; — a library consumed by external teams on strict versioned contracts may live better as its own repo with real published versions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tooling tax exceeds the sharing benefit&lt;/strong&gt; — a two-person team with two services gains little from Bazel. Path filters and discipline scale further than conference talks admit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice none of these are "the repo got big." Big is a CI design problem, and it's the one this guide solves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes that make monorepo CI miserable
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rebuilding everything "to be safe."&lt;/strong&gt; You've rebuilt the polyrepo's costs inside one repo and kept none of its isolation. Safety lives in the graph's correctness, not in redundant compute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path filters without the dependency expansion.&lt;/strong&gt; The shared-library change that skips its dependents' builds doesn't save time; it schedules a surprise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No owner for flaky tests.&lt;/strong&gt; In a monorepo, one team's flake is every team's blocked Friday. Quarantine with an owner and an expiry, or fix it the week it appears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caches with undeclared inputs.&lt;/strong&gt; The first poisoned artifact costs more trust than a year of cache misses. Hermetic or don't cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A mega-workflow file.&lt;/strong&gt; One 2,000-line workflow that every PR triggers is un-reviewable and un-skip-able. Fan out per project; share logic through reusable workflows, not through one file's job list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping the merge queue because PR checks "already passed."&lt;/strong&gt; They passed against yesterday's main. The queue tests the combination you're actually shipping.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need Nx/Turborepo/Bazel, or are path filters enough?&lt;/strong&gt;&lt;br&gt;
Path filters are enough while the dependency graph is shallow enough to hold in your head — a few projects, one shared layer. The moment filters start missing dependents (or including everything "just in case"), the graph has outgrown hand-maintenance and a tool should own it. Start at the cheapest level that tells the truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I handle a change to the repo root — CI config, root lockfile?&lt;/strong&gt;&lt;br&gt;
Treat root-level changes as affecting everything that consumes them: a root CI change should run the full suite (it's the pipeline changing itself), and a lockfile change affects every project resolving from it. Encode that as an explicit filter rule rather than than discovering it during an incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monorepo with multiple languages — one tool or several?&lt;/strong&gt;&lt;br&gt;
Either can work. Bazel and Pants are built for the polyglot case; language-native tools (Nx for JS, plus per-language CI for the rest) work when the language subtrees share little code. The deciding factor is how much cross-language dependency actually exists — measure it before choosing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a monorepo slow down git itself?&lt;/strong&gt;&lt;br&gt;
Eventually, at very large scale, clone and status operations grow — which is why the largest monorepos pair with partial clone, sparse checkout, and virtual filesystems. Most teams hit CI pain years before git pain; solve them in that order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does this fit platform engineering?&lt;/strong&gt;&lt;br&gt;
The affected-only pipeline is a textbook platform product: a paved road every team in the repo consumes, maintained once. It's one of the highest-leverage builds in the &lt;a href="https://www.devopsenginer.com/blog/rise-of-platform-engineering-redefining-devops-2025" rel="noopener noreferrer"&gt;platform engineering&lt;/a&gt; story — internal customers, a real interface, and a measurable benefit (pipeline minutes) that no team has to build for itself.&lt;/p&gt;

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

&lt;p&gt;A monorepo's promise is atomic change across everything you own; its tax is a pipeline that wants to rebuild everything you own. Affected-only delivery is how you keep the promise and refuse the tax: detect the change, expand through the graph, build and test that slice, cache aggressively but hermetically, and let a merge queue guarantee that green PRs compose into a green main. Start where you are — for most GitHub Actions users that's path filters this week and an honest look at your shared packages — and let the graph's real complexity, not fashion, tell you when it's time for a build tool. If you want the underlying Actions mechanics first, our &lt;a href="https://www.devopsenginer.com/blog/how-to-build-cicd-pipeline-github-actions-2025" rel="noopener noreferrer"&gt;GitHub Actions pipeline guide&lt;/a&gt; is the place to start.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>githubactions</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
