FreeLens Extensions in 2026: The Complete Catalogue and How to Install Them

FreeLens Extensions in 2026: The Complete Catalogue and How to Install Them

FreeLens Extensions: The Short Answer

FreeLens installs extensions from npm. Open the Extensions page (⌘⇧E on macOS, Ctrl+Shift+E on Linux and Windows), paste the package name — for example @freelensapp/resource-map-extension — and press Install. There is no curated in-app marketplace yet, so the package name is the address: you get it from the extension’s repository or its npm page.

Two things trip up almost everyone arriving from OpenLens. First, @freelensapp/freelens-node-pod-menu is deprecated and now ships as an empty package — installing it does nothing, because that functionality moved into FreeLens itself. Second, OpenLens and Lens extensions are not drop-in compatible; several have been ported, but you install the FreeLens port, not the original.

This catalogue is current as of FreeLens v1.10.3 (July 2026). If you are still deciding which client to standardise on, see FreeLens vs OpenLens vs Lens.

How to Install a FreeLens Extension

  1. Open FreeLens and go to File → Extensions, or press ⌘⇧E / Ctrl+Shift+E.
  2. Paste the npm package name into the input field — the scoped name in full, including the @freelensapp/ prefix where it applies.
  3. Press Install. FreeLens downloads the package from the npm registry and unpacks it.
  4. Wait for the extension to appear in the installed list with its Enabled toggle on. Some extensions add a new entry to the left sidebar; others add menu items to existing resource views, so a missing sidebar icon does not mean the install failed.

Extensions are per-user, not per-cluster: once installed, an extension is available for every cluster in your catalogue. If an extension needs CRDs that a given cluster does not have — the Karpenter or FluxCD extensions, for instance — its views simply stay empty on that cluster rather than erroring.

Official FreeLens Extensions

These live under the freelensapp GitHub organisation and are maintained alongside the app itself.

Extensionnpm packageWhat it gives you
FluxCD@freelensapp/fluxcd-extensionKustomizations, HelmReleases, GitRepositories and sources as first-class views, with reconciliation state. The most adopted extension by a wide margin
AI@freelensapp/ai-extensionAn LLM assistant inside the IDE for explaining resources and drafting manifests
Gateway API@freelensapp/gateway-api-extensionGateways, HTTPRoutes and GatewayClasses — the successor to Ingress
Resource Map@freelensapp/resource-map-extensionLive force-directed graph of resources and their relations. Ported from the OpenLens original
Karpenter@freelensapp/karpenter-extensionNodePools, NodeClaims and provisioning decisions for the node autoscaler
Kamaji@freelensapp/kamaji-extensionHosted control planes managed by Kamaji
Sveltos@freelensapp/sveltos-extensionAdd-on distribution across fleets of clusters
Agent Bridge@freelensapp/agentbridge-extensionExposes the cluster to an external AI agent (Claude, Copilot, OpenCode)
For Claude@freelensapp/for-claude-extensionClaude-specific integration, still early (0.x)
Node & Pod Menu@freelensapp/freelens-node-pod-menuDeprecated — empty package. Do not install it

If you run Karpenter, the extension pairs naturally with the trade-offs covered in Karpenter vs Cluster Autoscaler — seeing NodeClaims appear and consolidate in real time makes the disruption behaviour much easier to reason about than reading events.

Community Extensions

Maintained by individuals outside the core organisation. They are genuinely useful, but check the version number before you rely on one in a work setting — several are still 0.x.

Extensionnpm packageWhat it gives you
Credential Explorerfreelens-credential-explorerSecret health and ownership: age, expiry, and the Secret → Deployment → Pods → ServiceAccount chain
Duplicate@omarluq/freelens-duplicate-extensionClones a running pod into a debug copy without editing manifests
Argo@sebastian-prokesch/freelens-argo-extensionArgo CD Applications and Argo Workflows resources
Workload Topologyfreelens-workload-topologyTopology view focused on workloads rather than the whole resource graph
Pod File Browserfreelens-pod-filebrowserBrowses a pod’s filesystem without dropping into kubectl exec
JobSetfreelens-jobset-extensionJobSet resources, relevant for batch and ML workloads

Spotting Expiring Tokens, Certificates and Stale Secrets

This is the single most common thing people go looking for an extension to solve, and stock FreeLens does not do it: the Secrets view shows you that a Secret exists, not whether the thing inside it has expired. A TLS certificate three days from expiry looks exactly like one with a year left.

Credential Explorer (freelens-credential-explorer) is the extension aimed squarely at that gap. It builds a dashboard around the questions you actually ask during an incident: how old is this Secret, when does it expire, which namespace is it in, and what is consuming it — following the chain from Secret through Deployment and Pods to the ServiceAccount. It recognises credentials from Vault, External Secrets, Argo CD, cert-manager and TLS certificates, Helm releases, and plain Opaque secrets.

Two caveats worth knowing before you install it: it declares itself a proof of concept at 0.1.x, and it requires FreeLens v1.9.0 or later. For a cluster where certificate expiry is a genuine production risk, treat it as a fast triage view rather than as your alerting — the durable answer is a cert-manager alert or a Prometheus rule on certmanager_certificate_expiration_timestamp_seconds, which pages you at three in the morning when a desktop IDE cannot.

Why freelens-node-pod-menu Does Nothing Any More

Early FreeLens shipped without the node and pod context menus that OpenLens users expected — shell into a pod, cordon a node, view logs from the right-click menu. The gap was filled by a separate extension, @freelensapp/freelens-node-pod-menu, and just about every migration guide written in 2025 tells you to install it.

That functionality has since been absorbed into FreeLens itself. The package still exists on npm and still installs cleanly, but it is now an empty package that provides nothing. If you followed an older guide and are wondering why the extension appears installed yet contributes no visible feature, this is why — and the menus you were after are already there in the core app. Uninstall it and move on.

Can You Install OpenLens or Lens Extensions?

Not directly. FreeLens forked from OpenLens, so the extension API is closely related and porting is usually mechanical, but the packages are published separately and an original Lens extension is not guaranteed to load. In practice a good number of the popular OpenLens extensions have already been ported — the resource map is the obvious example — and the pattern is to look for a freelens- flavoured package rather than to force the original.

If the extension you depend on has no port, forking and adapting it is a realistic afternoon of work where the licence permits, and the project publishes an example extension as a starting template. That is a meaningful difference from Lens Desktop, where the licensing of the binaries is the constraint rather than the code.

When an Extension Will Not Install

  • Nothing happens after pressing Install. Check the package name character for character — the scope matters, and @freelensapp/resource-map-extension and freelens-resource-map-extension are not the same string. The npm name is authoritative, not the GitHub repository name.
  • The extension installs but its views are empty. Almost always missing CRDs on that cluster. The FluxCD, Karpenter, Gateway API, Sveltos and Kamaji extensions all render nothing useful against a cluster that does not run the corresponding controller.
  • It installed but added no sidebar entry. Not every extension adds one; some only extend existing detail panes and context menus.
  • It worked before an upgrade and now does not. Community extensions track FreeLens releases loosely. Pin your FreeLens version if an extension is load-bearing for your workflow, and check the extension’s declared minimum version before upgrading the app.

Frequently Asked Questions

How do I install extensions in FreeLens?

Open the Extensions page with ⌘⇧E on macOS or Ctrl+Shift+E on Linux and Windows, paste the extension’s npm package name (for example @freelensapp/fluxcd-extension) and press Install. FreeLens pulls the package straight from the npm registry — there is no separate marketplace to browse, so the package name is what you need.

Do OpenLens and Lens extensions work in FreeLens?

Not as-is. FreeLens forked from OpenLens so the extension API is close and porting is usually straightforward, but packages are published separately and an original Lens extension is not guaranteed to load. Look for a FreeLens port — many popular OpenLens extensions, including the resource map, already have one.

Why does the freelens-node-pod-menu extension do nothing?

Because it is deprecated and now ships as an empty package. It existed to add back node and pod context menus that early FreeLens lacked; that functionality has since moved into the core app. Older migration guides still recommend installing it — you can safely uninstall it, the menus are already there.

Is there a FreeLens extension that shows when Secrets or certificates expire?

Yes — freelens-credential-explorer. It reports Secret age and expiry and maps the Secret → Deployment → Pods → ServiceAccount chain, recognising credentials from Vault, External Secrets, Argo CD, cert-manager, Helm and plain Opaque secrets. It is a 0.1.x proof of concept and needs FreeLens v1.9.0 or later, so use it for triage rather than as your expiry alerting.

Are FreeLens extensions installed per cluster or per user?

Per user. An installed extension is available across every cluster in your catalogue. If a cluster lacks the CRDs an extension depends on — Flux, Karpenter, Gateway API — its views stay empty on that cluster instead of failing.

Which FreeLens extension is the most widely used?

The FluxCD extension, by a clear margin. It surfaces Kustomizations, HelmReleases, GitRepositories and their reconciliation state as first-class views, which is the single biggest gap in stock FreeLens for teams running GitOps.

Further Reading

Kubernetes Features That Hurt in Production: A Framework for Safer Adoption

Kubernetes Features That Hurt in Production: A Framework for Safer Adoption

If you manage Kubernetes in production, you’ve likely felt the sting of a feature that promised stability but delivered chaos. The community is rich with stories of PodDisruptionBudgets (PDBs) that blocked critical updates, misconfigured liveness probes that created restart loops, and resource limits that turned a simple deployment into a cascading failure. These aren’t inherently “bad” features; they are powerful tools that, like a surgeon’s scalpel, require precise understanding and context to use effectively.

Related reading: liveness probe anti-patterns.

Drawing from a wealth of shared experience—including community discussions and documented failure stories—a clear pattern emerges. The gap between a feature’s theoretical promise and its production reality is often bridged not by more documentation, but by operational rigor. This post analyzes common pitfalls, not to discourage the use of these features, but to provide a decision framework for platform teams to evaluate adoption, focusing on observability, gradual rollout, and clear rollback plans.

The Gap Between Theory and Practice: Features That Bite Back

Kubernetes is designed to automate complex distributed systems patterns. However, this automation can amplify misconfigurations at scale. The following features are frequently cited as sources of production pain, precisely because their power is double-edged.

1. PodDisruptionBudgets (PDBs): The Update Blocker

On paper, a PDB is a safeguard. It ensures a minimum number of pods for a critical application remain available during voluntary disruptions like node drains or cluster upgrades. The theory is flawless.

The practice, as shared by many engineers, reveals the trap: a PDB with overly restrictive minAvailable or maxUnavailable settings can completely halt cluster maintenance. Imagine a deployment with 3 pods and a PDB set to minAvailable: 3. Any drain operation is now impossible, stalling node security patches or Kubernetes version upgrades. The cluster’s ability to heal and evolve is held hostage by a configuration intended to protect it.

The deeper lesson isn’t to avoid PDBs, but to configure them with the system’s evolution in mind. They must allow for the cluster’s own lifecycle operations.

2. Liveness and Readiness Probes: The Self-Inflicted Outage

Probes are the cornerstone of Kubernetes’ self-healing and traffic management. A liveness probe failure restarts the pod; a readiness probe failure removes it from service endpoints. This is essential for resilience.

In production, misconfigured probes are a classic source of instability. Common pitfalls include:

  • Overly sensitive liveness checks: A probe checking an endpoint that briefly spikes in latency due to a downstream cache miss can cause a restart loop, exacerbating the problem and taking the service fully down.
  • Resource-intensive probes: A probe that executes a heavy database query every few seconds can itself become a source of resource exhaustion and latency, creating a feedback loop of failure.
  • Incorrect readiness signals: An application marked “not ready” during its entire startup or lengthy initialization will never receive traffic, appearing as a deployment failure.

As noted in the Kubernetes configuration overview, probes must be designed to reflect the actual health of the application, not an idealized state. They should be cheap, stable, and representative.

3. Resource Requests and Limits: The Silent Strangulation

Setting CPU and memory requests/limits is Kubernetes 101. They ensure fair scheduling and prevent a single pod from consuming all node resources. The theory is fundamental to multi-tenancy.

The production reality is subtler. Setting limits too low (“limit starvation”) is a frequent cause of mysterious, intermittent failures. A pod hitting its CPU limit is throttled, causing increased latency and timeouts. A pod hitting its memory limit is OOMKilled instantly. The symptoms—slow responses or disappearing pods—often point to application bugs, masking the true infrastructure cause.

Conversely, setting requests too high leads to poor cluster utilization and scheduling headaches. The key is continuous observation: limits should be informed by actual usage under load, not initial guesses.

4. Helm Hooks and Complex Operators: The Unpredictable Orchestrator

Helm hooks and custom operators automate complex lifecycle tasks: database migrations, secret injection, or pre-upgrade validation. They abstract away imperative steps.

In production, this abstraction can become a black box. A post-install hook that fails can leave a release in a stuck state. An operator with a bug in its reconciliation logic can enter a loop, endlessly creating and deleting resources. The complexity of debugging an automated system that has gone awry often far exceeds the complexity of the manual process it replaced. The failure stories aggregated in resources like kubernetes-failure-stories are replete with examples of automation gone wrong.

A Framework for Safer Feature Adoption

Banning powerful features is not the answer. The goal is to adopt them with eyes wide open. Platform teams should implement a framework that evaluates risk and mandates safeguards. Here is a practical, four-phase approach.

Phase 1: Evaluation & Contextual Understanding

Before enabling a feature cluster-wide or recommending it to application teams, ask:

  • What problem does this solve for us? Is it a real pain point, or just a “nice-to-have”?
  • What is the failure mode? How can this feature break? (e.g., PDBs block drains, probes cause restarts).
  • What are the observability requirements? What metrics, logs, and alerts do we need to see if it’s misbehaving?
  • What is the rollback procedure? How do we quickly disable or revert this feature if it causes an incident?

Phase 2: Implementation with Guardrails

Deploy the feature with constraints that limit its blast radius.

  • Start with non-critical workloads: Apply PDBs first to staging or low-priority services.
  • Use sane defaults via Policy-as-Code: Use tools like OPA/Gatekeeper or Kyverno to enforce safe defaults. For example, a policy could forbid PDBs with minAvailable: 100% or enforce a maximum probe timeout.
  • Document the “why” and the “how to escape”: Annotate resources or maintain runbooks that explain the configuration and the steps to neutralize it in an emergency.

Phase 3: Gradual Rollout & Observability

Treat feature adoption like a software deployment.

  • Canary the configuration: Apply a new PDB or aggressive probe to one pod or one namespace first. Monitor its effect closely.
  • Implement specific monitoring: Beyond general cluster health, create alerts for:
    – PDBs blocking evictions for > X minutes.
    – Pod restart counts spiking (potential probe issue).
    – Containers hitting CPU throttling or being OOMKilled.
    – Helm releases stuck in a pending hook state.

A simple Prometheus alert for PDB blockage might look like this:

# Alert if a PDB is blocking voluntary pod disruptions for too long
- alert: PDBBlockingDisruption
  expr: kube_poddisruptionbudget_status_current_healthy == kube_poddisruptionbudget_status_desired_healthy
    and (kube_poddisruptionbudget_status_desired_healthy - kube_poddisruptionbudget_status_expected_pods) == 0
    and kube_poddisruptionbudget_status_disruptions_allowed == 0
  for: 10m
  labels:
    severity: warning
  annotations:
    summary: "PDB {{ $labels.namespace }}/{{ $labels.poddisruptionbudget }} is blocking all pod disruptions"
    description: "The PDB requires all pods to be available, preventing node drains or updates for 10 minutes."

Phase 4: Review and Iteration

Adoption isn’t a one-time event. Regularly review:

  • Are the features providing the intended value? Are PDBs actually increasing availability during updates?
  • What incidents or near-misses have they been involved in? Use post-incident reviews to refine configurations and policies.
  • Can we improve defaults or abstractions? Can the platform team provide a simplified, safe Custom Resource or Helm chart that encapsulates best practices?

Frequently Asked Questions

Which Kubernetes features cause the most production incidents?

The recurring offenders are the ones that act automatically on your behalf: PodDisruptionBudgets that block node drains forever, liveness probes that restart healthy pods under load, and aggressive affinity rules that make workloads unschedulable. None of them are bad features — they hurt when adopted with defaults copied from a tutorial instead of settings derived from your workload.

Why is a PodDisruptionBudget risky if it protects availability?

Because a PDB with maxUnavailable: 0 (or a selector matching a single replica) makes voluntary disruption impossible: node drains hang, upgrades stall, and cluster maintenance turns into manual pod deletion at 2 AM. A PDB must always leave the cluster a legal way to move your pods.

Should I avoid these features altogether?

No — the article’s point is the opposite. Adopt them deliberately: understand the failure mode each feature introduces, test that failure mode (drain a node, kill a replica) before production, and roll out with conservative settings you tighten over time. Power tools, respected.

How do I evaluate a Kubernetes feature before adopting it?

Four questions: What does it do automatically and when? What is the failure mode when it misfires — and does it fail open or closed? Can I observe it acting (events, metrics)? And can I roll it back under pressure? If you cannot answer all four, you are adopting a behavior, not a feature.

Conclusion: Embrace Power, Respect Complexity

The history of engineering is the history of building more powerful tools and learning to wield them safely. Kubernetes features like PDBs, probes, and resource management are no different. Their potential for causing production pain is a direct reflection of their power to automate complex, critical behaviors.

The path forward is not avoidance, but disciplined adoption. By shifting from a mindset of “enable and hope” to a framework of “evaluate, guard, observe, and iterate,” platform teams can harness these powerful features to build more resilient, self-healing systems without becoming victims of their own automation. The lessons are already written in the community’s failure stories; the task is to learn from them and build a safer, more informed practice.

Kubernetes Namespace Isolation: When Security Boundaries Fail

Kubernetes Namespace Isolation: When Security Boundaries Fail

If you manage multi-tenant Kubernetes clusters or enforce strict separation between development, staging, and production environments, you likely rely on namespaces as a primary security boundary. It’s a logical, clean model: workloads and users are isolated within their designated namespaces, creating a clear perimeter for access control and network traffic.

Related reading: the 2026 Kubernetes hardening guide.

But what if this boundary is more of a suggestion than a hard wall? The reality is that namespace isolation, as defined by the Kubernetes security model, is a construct built on the correct configuration of several underlying controls. When one of those controls fails—often through misconfigured platform-level tools—the entire boundary can collapse, enabling lateral movement that compromises the security of your entire cluster.

This isn’t a theoretical concern. Recent vulnerabilities, like the one detailed in a discussion on CVE-2026-22039, demonstrate how admission controllers, which are meant to enforce security, can be exploited to bypass namespace isolation entirely. This event isn’t an anomaly; it’s a symptom of a broader pattern where the complexity of the platform layer introduces critical gaps in a core security tenet.

The Illusion of the Hard Boundary

Kubernetes documentation is clear: “Namespaces are a way to divide cluster resources between multiple users.” They are a mechanism for scoping names and organizing objects. However, they are not a security feature by themselves. Isolation is achieved through the combination of:

  • RBAC (Role-Based Access Control): Governing who can do what, and where.
  • Network Policies: Controlling pod-to-pod communication.
  • Admission Controllers: Validating and mutating requests before persistence.
  • Resource Quotas & Limit Ranges: Managing resource consumption.

The security boundary exists only when all these layers are correctly configured and aligned. A flaw in any one layer—especially in a cluster-scoped component like an admission controller, a monitoring agent, or a service mesh sidecar injector—can create a bridge between namespaces.

How the Boundary Fails: Real-World Exploit Paths

Let’s examine common failure modes that break namespace isolation, moving from conceptual to concrete.

1. Privileged Admission Controller Exploits

The CVE-2026-22039 discussion highlights a classic case. An admission controller with broad permissions (e.g., cluster-admin or powerful ClusterRole bindings) is deployed to validate resources. If this controller has a vulnerability—such as improperly validating the requesting user or namespace of the mutated object—an attacker could craft a request that tricks the controller into creating or modifying resources in a namespace they should not have access to.

Attack Flow:

  1. Attacker has compromised a pod in the tenant-a namespace.
  2. They discover a cluster-scoped admission controller (e.g., a policy engine) is vulnerable to a confused deputy attack.
  3. They send a malicious payload to the API server that triggers the admission controller.
  4. The admission controller, operating with high privileges, is tricked into creating a privileged ServiceAccount or a pod with host network access in the kube-system namespace.
  5. Isolation is broken; lateral movement to a critical namespace is achieved.

2. Misconfigured Network Policies (or Their Absence)

By default, Kubernetes networking allows all pods to communicate with each other, regardless of namespace. Without Network Policies, a compromised pod in dev can directly probe and attack pods in production. Even with policies, a single overly permissive rule (e.g., allowing ingress from all namespaces for a debugging port) can create a breach.

3. Over-Permissioned Service Accounts & Pods

ServiceAccounts are namespaced, but the RBAC Roles or ClusterRoles bound to them are not. A common misconfiguration is binding a namespaced ServiceAccount to a powerful ClusterRole. A pod using that ServiceAccount effectively has those cluster-wide privileges, allowing it to read secrets, delete pods, or create bindings in any namespace.

A Framework for Testing Namespace Boundaries

Assuming isolation is dangerous. You must actively test it. Here is a practical framework for platform and security teams.

Phase 1: Discovery & Mapping

  • Inventory Cluster-Scoped Components: List all deployments, daemonsets, and pods in kube-system, gatekeeper-system, istio-system, etc. Document their assigned ServiceAccounts and associated RBAC.
  • Audit RBAC Bindings: Use kubectl get clusterrolebindings -o wide and kubectl get rolebindings --all-namespaces to find any bindings of powerful roles to ServiceAccounts or users in non-critical namespaces.
  • Map Network Policies: Generate a visual or logical map of allowed ingress/egress flows between namespaces. Identify namespaces with no policies applied.

Phase 2: Active Penetration Testing

From the perspective of a compromised pod in a non-privileged namespace (simulate with a benign test pod), run controlled tests.

Test for Privilege Escalation:

# Inside the test pod
# 1. Check the pod's own permissions
kubectl auth can-i --list

# 2. Attempt to list resources in other namespaces
kubectl get pods -n kube-system
kubectl get secrets -n production

# 3. Attempt to create a pod in another namespace
cat <<EOF | kubectl apply -f - --namespace=kube-system
apiVersion: v1
kind: Pod
metadata:
  name: test-breakout
spec:
  containers:
  - name: busybox
    image: busybox
    command: ['sh', '-c', 'sleep 3600']
EOF

Test for Network Access:

# Use netcat or curl to probe internal services in other namespaces
# Find the ClusterIP of a service in the target namespace
kubectl get svc -n production --output=wide

# From test pod, attempt to connect
curl -v http://<production-service-cluster-ip>:<port>
nc -zv <production-service-cluster-ip> <port>

Phase 3: Admission Controller Stress Testing

This is more advanced but critical. Craft resource manifests designed to probe the logic of your policy engines (e.g., OPA Gatekeeper, Kyverno, custom webhooks).

  • Submit requests with mismatched or spoofed namespace fields in the object metadata versus the request path.
  • Attempt to create objects that reference resources (like ConfigMaps or Secrets) in other namespaces.
  • Test if controllers correctly validate the userInfo (username, groups) of the requester in their decision-making logic.

Hardening the Boundary: Defensive Controls

Testing reveals gaps; these controls close them.

1. Implement Zero-Trust Networking

Default-deny is the only sane starting point. Apply a baseline NetworkPolicy to every namespace that denies all ingress from other namespaces.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-cross-namespace
  namespace: <protected-namespace>
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector: {} # Only allow from pods in the SAME namespace

Explicitly allow required cross-namespace communication (e.g., from monitoring or service mesh namespaces) using namespaceSelector rules, not open CIDR blocks.

2. Principle of Least Privilege for Platform Tools

Scrutinize the RBAC for every cluster-scoped component. Does your admission controller really need create permission on pods cluster-wide? Or can its role be scoped to specific namespaces or resource types? Use tools like kubectl audit or third-party RBAC analyzers to find and reduce over-permissive bindings.

3. Namespace-as-a-Boundary for ServiceAccounts

Never bind a namespaced ServiceAccount to a ClusterRole unless it is absolutely necessary. If a component needs to act in multiple namespaces, create separate ServiceAccounts and RoleBindings in each namespace, or use a tightly scoped ClusterRole that only lists the required namespaced resources.

Monitoring for Boundary Violations

Detection is your last line of defense. Monitor the Kubernetes API audit logs for tell-tale signs of boundary crossing.

  • Unauthorized Cross-Namespace API Calls: Alert on create, update, or get requests where the requesting user/service account’s namespace differs from the target object’s namespace, and the action is not explicitly allowed by a known pattern (e.g., cluster-admin, system components).
  • Network Policy Violation Attempts: If using a CNI that supports it (like Cilium), export flow logs and alert on denied connection attempts from one namespace to a sensitive another.
  • Admission Controller Anomalies: Monitor the logs of your admission webhooks for a high rate of denials or errors, which could indicate probing or exploit attempts.

Frequently Asked Questions

Are Kubernetes namespaces a security boundary?

Not by default. A namespace is a logical grouping for names, quotas and RBAC scoping — but the network is flat, and nothing stops a pod in one namespace from calling a Service in another. Namespaces only become a security boundary when you actively enforce one: default-deny NetworkPolicies, scoped RBAC, and admission controls.

Can pods in different namespaces communicate by default?

Yes — any pod can reach any other pod or Service across namespaces out of the box (service.other-ns.svc.cluster.local). If your threat model assumes tenant separation, that default is the first thing to close with a default-deny NetworkPolicy per namespace.

What commonly escapes namespace isolation?

Cluster-scoped resources (nodes, CRDs, ClusterRoles), anything with host access (hostPath, hostNetwork, privileged pods), the shared kernel itself, and RBAC grants that quietly cross namespaces (ClusterRoleBindings). Auditing those is more valuable than adding more controls inside the namespace.

How do I actually harden namespace boundaries?

Layered: default-deny NetworkPolicy (ingress and egress) per namespace, Roles instead of ClusterRoles wherever possible, ResourceQuota and LimitRange to contain blast radius, Pod Security Admission at restricted, and periodic boundary testing — the article’s framework — to verify the isolation you think you have actually holds.

Conclusion: Isolation as an Active Discipline

Namespace isolation in Kubernetes is not a static configuration you set and forget. It is a dynamic security property that must be continuously validated, enforced, and monitored. The platform’s complexity and extensibility, while powerful, are the very factors that introduce fragility into this boundary.

The deprecation of a component like Ingress-NGINX teaches us to build frameworks for change. Similarly, breaches of namespace isolation teach us that security is not a feature of a single object (the namespace), but an emergent property of the entire system’s configuration. Your cluster’s security boundary is only as strong as the weakest link in the chain of RBAC, network policies, and admission control. Treat it as a critical, living part of your platform—one that demands proactive testing, rigorous hardening, and vigilant observation.

Lazy-Pulling in Containerd v2: Why Pull Time Isn’t the Right Metric

Lazy-Pulling in Containerd v2: Why Pull Time Isn't the Right Metric

If you’re running containerized workloads at scale, you’ve likely encountered the cold-start problem: a pod gets scheduled, the container runtime begins pulling a multi-gigabyte image, and your application sits idle while layers download. This latency directly impacts developer velocity, autoscaling responsiveness, and user experience.

Related reading: daemonless image tooling with Skopeo, Crane and regctl.

Containerd’s lazy-pulling feature (also known as on-demand pulling or stargz) promises to solve this by fetching only the metadata initially, then downloading individual file chunks as the container accesses them. The promise is compelling—near-instant container startup regardless of image size. However, the community conversation around this feature has been dominated by a misleading metric: pull time.

In this article, we’ll dismantle the pull time benchmark fallacy and examine the real performance characteristics that matter for production workloads. We’ll explore how lazy-pulling actually works under the hood, what metrics you should be measuring instead, and when this technology makes sense for your infrastructure.

How Lazy-Pulling Actually Works in Containerd

Before we can understand the metrics, we need to understand the mechanism. Traditional container image pulling follows a sequential, all-or-nothing approach:

  1. The container runtime requests an image manifest.
  2. It downloads all layer blobs (compressed tarballs) to local storage.
  3. It decompresses and extracts all layers to create the final root filesystem.
  4. Only then can the container start.

Lazy-pulling fundamentally changes this workflow. Based on the containerd operations documentation and community implementations, the process looks more like this:

  1. The runtime fetches the image manifest and a specialized index file that maps file paths to byte ranges within compressed layers.
  2. It prepares a virtual filesystem that appears to contain the complete image.
  3. When a process inside the container attempts to read a file, the filesystem driver intercepts the request.
  4. It consults the index, fetches only the specific byte ranges needed for that file from the registry, decompresses them on-the-fly, and serves the data to the application.
  5. Fetched chunks are cached locally to avoid redundant network requests.

This approach is conceptually similar to how modern game engines stream assets or how HTTP range requests enable video streaming. The key insight is that most containers only need a small subset of their total image content during initial startup.

The Pull Time Fallacy: What Traditional Benchmarks Get Wrong

When lazy-pulling first gained attention, the most common benchmark comparison was simple: “How long does it take to pull a 1GB image versus lazy-pull the same image?” This metric is not just incomplete—it’s actively misleading for several reasons.

1. Pull Time Measures the Wrong Phase

With traditional pulling, the pull command blocks until all layers are downloaded and extracted. With lazy-pulling, the pull command returns almost immediately after fetching metadata. Comparing these two durations is like comparing the time to download a movie trailer versus the time to buffer the entire film—they’re measuring fundamentally different operations.

The meaningful comparison isn’t pull time versus lazy-pull time. It’s total time to application readiness. This includes:

  • Metadata fetch time
  • Container initialization time
  • Application bootstrap time (where file accesses actually happen)
  • Any network latency incurred during on-demand fetching

2. It Ignores Workload Access Patterns

As detailed analysis has shown, the performance impact of lazy-pulling depends entirely on your container’s filesystem access patterns during startup. Consider two extreme examples:

  • Node.js application: Starts quickly, requires few files (node binary, package.json, a handful of .js files). Lazy-pulling provides near-instant startup.
  • Java application with large classpath: Scans hundreds of JAR files during initialization, triggering many small reads across the filesystem. This could result in hundreds of HTTP range requests, adding significant latency.

A single “pull time” metric cannot capture this complexity. The real question is: How does the filesystem access pattern during application startup map to the image’s layer structure?

3. It Misses the Caching Benefit

Traditional pulling has a clear caching model: once a layer is pulled, subsequent containers using that layer start instantly (assuming no registry changes). Lazy-pulling introduces a more nuanced caching model where individual file chunks are cached after first access.

This means the performance of lazy-pulling improves for frequently accessed files across container instances, but benchmarking typically focuses on cold starts only.

The Metrics That Actually Matter for Production

To properly evaluate lazy-pulling for your workloads, you need to measure these key performance indicators:

1. Time-to-First-Byte (TTFB) for Application Requests

This is the most critical metric for user-facing services. How long from kubectl apply or horizontal pod autoscaler decision until your application can serve its first request? This metric encompasses:

  • Pod scheduling
  • Image provisioning (metadata fetch for lazy-pulling)
  • Container runtime initialization
  • Application startup (including any on-demand file fetching)
  • Health check passes

For autoscaling scenarios, TTFB directly impacts your ability to handle traffic spikes.

2. Application Startup Latency Breakdown

Instead of measuring “pull time,” instrument your application to report key milestones:

# Example instrumentation points for a web application
START_TIME=$(date +%s.%N)
# ... application code ...
CONTAINER_READY=$(date +%s.%N)  # Runtime reports container running
IMPORTS_LOADED=$(date +%s.%N)   # After requiring dependencies
DATABASE_CONNECTED=$(date +%s.%N) # After DB connection established
HTTP_LISTENING=$(date +%s.%N)   # Server listening on port

By comparing these timestamps between traditional and lazy-pulled containers, you can identify exactly where latency is introduced or saved.

3. Network Request Patterns During Startup

Monitor the filesystem layer to understand what’s being fetched:

  • How many HTTP range requests occur during startup?
  • What’s the total bandwidth transferred versus the full image size?
  • What’s the request pattern (sequential vs random access)?
  • How effective is the local chunk cache?

This data helps you optimize image construction. If your application sequentially reads every file in /usr/lib during startup, you might want to ensure those files are contiguous in the image layer to minimize range requests.

4. Registry Load and Scaling Characteristics

Lazy-pulling changes the load pattern on your container registry. Instead of occasional large downloads, you get frequent small range requests. You need to measure:

  • Registry requests per second during scaling events
  • Cache hit ratios at registry CDN level
  • Impact on registry authentication systems (more requests = more token validation)

Implementation Considerations and Gotchas

Based on the Kubernetes images documentation and containerd’s implementation, here are practical considerations for adopting lazy-pulling:

Compatibility and Requirements

Lazy-pulling requires specific configuration and compatible images:

  • Runtime support: Containerd with appropriate snapshotter (e.g., stargz, overlaybd)
  • Image format: Images must be built with lazy-pulling support (e.g., using ctr-remote or nerdctl with stargz compression)
  • Registry support: The registry must support HTTP range requests (most do, but verify)
  • Kubernetes version: No specific version requirement, but newer kubelets have better integration

When Lazy-Pulling Excels

  • Large base images with small applications: e.g., a 2GB ubuntu:latest base running a 50MB Go binary
  • Ephemeral workloads: CI/CD runners, batch jobs, function-as-a-service platforms
  • Memory-constrained environments: Lazy-pulling avoids decompressing entire layers to disk
  • Autoscaling scenarios: When you need to scale from zero quickly

When Traditional Pulling May Be Better

  • Applications with random access patterns: Databases that read many files during startup
  • Network-limited environments: If each range request has high latency, the overhead adds up
  • Air-gapped or registry-constrained deployments: Predictable bandwidth usage may be preferable
  • Images with poor locality: Files accessed together aren’t stored together in the image

Practical Recommendations for Platform Teams

1. Profile Before You Optimize

Don’t implement lazy-pulling globally based on synthetic benchmarks. Start with profiling:

  1. Identify your most frequently scaled or restarted workloads
  2. Measure their actual file access patterns during startup (using strace or eBPF tools)
  3. Build test images with lazy-pulling support
  4. A/B test against traditional images in staging
  5. Measure the real metrics: TTFB, resource usage, registry load

2. Optimize Image Construction

Lazy-pulling performance depends heavily on image layout. Follow these guidelines:

  • Place files accessed during startup close together in the layer
  • Keep startup dependencies in as few layers as possible
  • Consider separating runtime dependencies from build dependencies
  • Use multi-stage builds to minimize final image size

3. Implement Gradual Rollout

When deploying lazy-pulling in production:

  1. Start with non-critical, ephemeral workloads
  2. Implement comprehensive monitoring for both performance and errors
  3. Set up alerts for abnormal request patterns to your registry
  4. Have a rollback plan (traditional images still available)

4. Monitor the Right Things

Beyond application metrics, monitor infrastructure impacts:

  • Registry request rate and latency percentiles
  • Node filesystem cache efficiency
  • Network bandwidth patterns (many small requests vs few large ones)
  • Container startup failure rates

The Future of Container Image Distribution

Lazy-pulling represents a fundamental shift from treating container images as monolithic blobs to treating them as structured, queryable datasets. This evolution will likely continue with:

  • Intelligent prefetching: Systems that learn access patterns and prefetch likely-needed chunks
  • Content-defined chunking: More efficient than fixed-range requests
  • Peer-to-peer distribution: Nodes sharing fetched chunks locally
  • Integration with GitOps: Knowing which image versions will be needed based on Git changes

The key takeaway is that we’re moving beyond simple “pull time” optimization toward holistic container lifecycle optimization. The metric that matters isn’t how fast we can download bytes, but how quickly we can deliver working applications to users.

Frequently Asked Questions

What is lazy-pulling in containerd?

Starting a container before the full image has been downloaded: the snapshotter mounts the image and fetches file chunks on demand from the registry as the process actually reads them. Formats like eStargz make this possible by making image layers seekable, so containerd can pull “just enough” instead of everything.

Does lazy-pulling make image pulls faster?

It makes time-to-ready faster, which is not the same thing. Total bytes transferred can end up similar or even higher; what changes is that the container starts in seconds instead of waiting minutes for a full pull. That is why judging it by classic pull-time benchmarks misses the point — the metric that matters is how quickly the workload is actually serving.

What do I need to enable lazy-pulling?

Two pieces: a lazy-capable snapshotter (like the stargz snapshotter) configured in containerd, and images converted to a seekable format — a build/CI step, since a standard OCI image cannot be lazily pulled. Both halves matter: the runtime alone does nothing for unconverted images.

What are the trade-offs of lazy-pulling in production?

First-access latency: files fetched on demand pay a network round-trip the first time they are read. Runtime registry dependency: if the registry is unreachable after start, a cold file read can fail — so registry availability becomes a runtime concern, not just a deploy-time one. And cache behavior changes capacity planning on the nodes.

Conclusion

Lazy-pulling in containerd v2 is a powerful technology that can dramatically improve container startup performance, but only if measured and implemented correctly. The traditional “pull time” benchmark is a relic of an earlier approach to container images and should be replaced with application-centric metrics like time-to-first-byte and startup latency breakdowns.

For platform engineers and SREs, the path forward is clear: profile your actual workloads, understand their filesystem access patterns, test lazy-pulling with realistic metrics, and implement gradually with proper monitoring. When applied to the right workloads with optimized images, lazy-pulling can transform your scaling responsiveness and resource efficiency.

Remember: the goal isn’t faster image pulls—it’s faster applications. Measure what matters.

Beyond GPU Drivers: Custom Node Readiness for Specialized Workloads

Beyond GPU Drivers: Custom Node Readiness for Specialized Workloads

If you manage Kubernetes clusters for specialized workloads—think AI/ML, high-performance computing, financial modeling, or real-time data processing—you know that a node being Ready in the Kubernetes sense is often just the starting point. The standard kubelet health checks ensure the node’s core services are running, but they say nothing about whether the specific, often expensive, hardware or software dependencies your workload requires are truly operational.

Related reading: GPU scheduling with Dynamic Resource Allocation.

You might have nodes with GPUs where the driver crashed, nodes with local NVMe volumes that haven’t finished formatting, or nodes where a mandatory security or monitoring agent is unhealthy. Scheduling a sensitive, resource-intensive pod onto such a node is a recipe for silent failures, degraded performance, or security non-compliance.

This is where the Node Readiness Controller moves from a convenience to a critical platform control plane component. While its introductory examples often focus on GPU driver checks, its real power lies in enforcing your platform’s unique definition of “ready.” In this article, we’ll move beyond the basics and explore how to implement custom node readiness gates for advanced, production-grade scenarios.

Why Standard Node Readiness Isn’t Enough

Kubernetes marks a node as Ready when its kubelet can communicate with the API server and reports that essential node conditions (like MemoryPressure, DiskPressure, PIDPressure, and the generic Ready) are false. This is a binary, infrastructure-level view.

For specialized workloads, you need to enforce application-level readiness. Consider these scenarios:

  • Local Storage Provisioning: A DaemonSet formats and mounts a local SSD. A pod requiring that fast storage must not schedule until the mount is confirmed writable.
  • Kernel Module Dependencies: Your workload needs a specific kernel module (e.g., rdma, nf_conntrack, a custom FPGA driver) loaded and configured. The node is healthy, but the module isn’t present.
  • Security & Compliance: Your security policy mandates that a host-based intrusion detection system (HIDS) or a data loss prevention (DLP) agent is running and heartbeating healthily on every node before any workload can run.
  • Hardware Attestation: In confidential computing or regulated environments, you may need cryptographic proof from a Trusted Platform Module (TPM) or AMD SEV-SNP that the node’s firmware and boot chain are in a known, trusted state.
  • External Resource Availability: The node needs access to a licensed software server, a specific network filesystem, or an external hardware security module (HSM).

Scheduling pods without these conditions met leads to runtime errors, inconsistent performance, or policy violations. The Node Readiness Controller, by adding custom conditions to the node’s status, allows you to define these gates explicitly. The Kubernetes scheduler will then treat a node as unschedulable until all conditions—both built-in and custom—are met.

Architecting Custom Readiness Checks

The pattern involves three key components:

  1. The Condition: A custom condition name (e.g., node.alexandre-vazquez.com/LocalStorageReady) you define and add to the node’s .status.conditions.
  2. The Checker: A controller or agent running on the node (often as a DaemonSet) that performs the actual validation (e.g., tests a mount, checks a kernel module, calls a health API).
  3. The Enforcer: The Node Readiness Controller itself, which watches for these conditions and applies a matching taint (e.g., node.alexandre-vazquez.com/local-storage-not-ready:NoSchedule) when the condition is False or Unknown.

Pods that require the specialized resource must then have a corresponding toleration for that taint. This creates a clean, declarative contract: the pod declares its dependency, and the platform guarantees the node meets the requirement before scheduling.

Practical Implementation: Node-Local Storage Readiness

Let’s implement a common pattern: ensuring a local NVMe volume is formatted, mounted, and performance-tested before allowing pods to claim it via a PersistentVolumeClaim.

First, we define a DaemonSet that runs an init container to prepare the storage and a main container that acts as the readiness checker. The checker will periodically validate the mount and update the node’s condition.

1. DaemonSet for Storage Provisioning and Health Checking

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: local-storage-readiness
  namespace: kube-system
spec:
  selector:
    matchLabels:
      name: local-storage-readiness
  template:
    metadata:
      labels:
        name: local-storage-readiness
    spec:
      hostPID: true
      containers:
      - name: checker
        image: alpine:latest
        command:
        - "/bin/sh"
        args:
        - "-c"
        - |
          # Function to update node condition
          update_condition() {
            local condition=$1
            local status=$2
            local message=$3
            local patch=$(cat <<EOF
            {
              "status": {
                "conditions": [
                  {
                    "type": "$condition",
                    "status": "$status",
                    "lastHeartbeatTime": "$(date -u +'%Y-%m-%dT%H:%M:%SZ')",
                    "reason": "LocalStorageCheck",
                    "message": "$message"
                  }
                ]
              }
            }
            EOF
            )
            curl -k -X PATCH -H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" -H "Content-Type: application/strategic-merge-patch+json" 
              "https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}/api/v1/nodes/$(cat /etc/hostname)/status" -d "$patch"
          }

          # Main check loop
          while true; do
            if mountpoint -q /mnt/local-ssd && [ -w /mnt/local-ssd ] && dd if=/dev/zero of=/mnt/local-ssd/test.bin bs=1M count=100 oflag=direct 2>&1 | grep -q 'records out'; then
              update_condition "LocalStorageReady" "True" "/mnt/local-ssd is writable and performant"
            else
              update_condition "LocalStorageReady" "False" "Local storage failed health check"
            fi
            sleep 30
          done
        securityContext:
          privileged: true
        volumeMounts:
        - name: local-ssd
          mountPath: /mnt/local-ssd
        - name: kube-api-access
          mountPath: /var/run/secrets/kubernetes.io/serviceaccount
          readOnly: true
      initContainers:
      - name: provisioner
        image: alpine:latest
        command: ["sh", "-c"]
        args:
          - mkfs.ext4 -F /dev/nvme0n1 && mkdir -p /mnt/local-ssd && mount /dev/nvme0n1 /mnt/local-ssd
        securityContext:
          privileged: true
        volumeMounts:
        - name: local-ssd
          mountPath: /mnt/local-ssd
        - name: device
          mountPath: /dev/nvme0n1
      volumes:
      - name: local-ssd
        hostPath:
          path: /mnt/local-ssd
          type: DirectoryOrCreate
      - name: device
        hostPath:
          path: /dev/nvme0n1
      - name: kube-api-access
        projected:
          sources:
          - serviceAccountToken:
              expirationSeconds: 3607
              path: token

2. NodeReadinessController Configuration

Now, configure the Node Readiness Controller to watch for our LocalStorageReady condition and apply a taint when it’s not True.

apiVersion: v1
kind: ConfigMap
metadata:
  name: node-readiness-controller-config
  namespace: kube-system
data:
  config.yaml: |
    conditions:
    - name: "LocalStorageReady"
      taint:
        key: "node.alexandre-vazquez.com/local-storage-not-ready"
        effect: "NoSchedule"
      state:
        true: "Ready"  # No taint when condition is True
        false: "NotReady"  # Apply taint when condition is False
        unknown: "NotReady" # Apply taint when condition is Unknown

3. Pod Specification with Toleration

A pod that requires this local storage must tolerate the taint, creating an explicit dependency.

apiVersion: v1
kind: Pod
metadata:
  name: data-processor
spec:
  tolerations:
  - key: "node.alexandre-vazquez.com/local-storage-not-ready"
    operator: "Exists"
    effect: "NoSchedule"
  containers:
  - name: app
    image: my-data-app:latest
    volumeMounts:
    - name: data
      mountPath: /data
  volumes:
  - name: data
    hostPath:
      path: /mnt/local-ssd/pod-data
      type: DirectoryOrCreate

This pattern ensures the pod only schedules onto nodes where the local storage health check has passed. The checker’s simple performance test (dd) helps catch degraded disks that are mounted but failing.

Integrating with Node Feature Discovery and CI/CD

For checks based on hardware capabilities, integrating with Node Feature Discovery (NFD) is powerful. Instead of writing custom hardware detection logic, you can use NFD to label nodes with features (e.g., feature.node.kubernetes.io/cpu-cpuid.AVX512BW: "true"). Your readiness checker can then verify that these expected labels are present and correspond to a working feature.

For example, a checker could:

  1. Use NFD labels to identify nodes with specific CPU extensions or GPUs.
  2. Run a micro-benchmark or diagnostic (e.g., nvidia-smi --query-gpu=health --format=csv).
  3. Set a condition like GpuHealthy to True or False based on the result.

This creates a two-stage validation: NFD discovers the hardware, and your readiness controller certifies its operational health.

CI/CD Pipeline Integration

In a GitOps-driven platform, custom readiness conditions should be treated as first-class citizens. Your deployment pipelines can include steps to verify that target nodes are not only Ready but also meet all custom conditions before proceeding:

# Example pipeline step using kubectl
- name: Validate Node Readiness for Workload
  run: |
    REQUIRED_CONDITIONS=(
      "LocalStorageReady"
      "SecurityAgentReady"
      "FirmwareAttested"
    )
    
    for condition in "${REQUIRED_CONDITIONS[@]}"; do
      if ! kubectl get node $TARGET_NODE -o jsonpath="{.status.conditions[?(@.type=='$condition')].status}" | grep -q "True"; then
        echo "Error: Node $TARGET_NODE not ready. Condition $condition not met."
        exit 1
      fi
    done
    echo "All custom readiness conditions met for $TARGET_NODE."

This proactive check in your CD pipeline can prevent deployments from hanging or failing due to unmet node-level dependencies.

Advanced Use Case: Security Agent Health Gate

Consider a security team that mandates the presence and health of a host-based agent (e.g., Falco, a commercial EDR). A readiness checker DaemonSet can query the agent’s local health API or UNIX socket. If the agent is unresponsive or reports a compromised state, the checker sets a SecurityAgentReady condition to False, tainting the node and preventing any new workloads from scheduling. This hardens your security posture by ensuring no pod runs on a node with a broken security control.

Best Practices and Considerations

  • Condition Naming: Use a domain prefix (e.g., yourcompany.com/) to avoid collisions with future Kubernetes built-in conditions.
  • Checker Resilience: The checker DaemonSet itself must be highly available and lightweight. Ensure it has appropriate resource requests/limits and a restart policy that maintains the check.
  • Taint Effects: Use NoSchedule to prevent new pods. For immediate response to a condition breaking on a running node, you might also consider NoExecute with a tolerationSeconds value on pods, but this requires careful design to avoid unnecessary churn.
  • Combining Conditions: A node can have multiple custom conditions. The scheduler respects all of them; a pod will only schedule if it tolerates all the taints applied due to unmet conditions.
  • Monitoring and Alerting: Treat custom condition states as critical metrics. Alert your platform team when a False condition persists, indicating a node-level issue requiring investigation.

Frequently Asked Questions

What does the Kubernetes node Ready condition actually check?

Only kubelet fundamentals: container runtime up, network configured, no memory/disk pressure. It says nothing about GPU drivers, CSI mounts, security agents or anything workload-specific — a node can be Ready while everything your pods need is still initialising. That gap is what custom readiness closes.

How do I stop pods from scheduling before the GPU driver is ready?

Register a startup taint that a node-local check removes once the driver responds (the pattern NVIDIA’s GPU Operator uses), or gate on the device plugin: pods requesting nvidia.com/gpu stay Pending until the plugin advertises capacity. For everything else — storage, agents — a DaemonSet health-checker that manages the taint is the general-purpose version.

What is Node Feature Discovery (NFD)?

A Kubernetes SIG project that inspects each node’s hardware and kernel (CPU flags, PCI devices, kernel modules) and publishes the result as node labels. Combined with node affinity, it lets workloads target capabilities instead of hand-maintained labels — and it is the discovery half of most custom-readiness setups.

Are node readiness gates a Kubernetes feature like pod readiness gates?

No — pods have first-class readinessGates; nodes do not. Node-level equivalents are built from primitives: taints applied at registration and removed by health checks, node conditions set by problem detectors (like Node Problem Detector), and labels from NFD. The article covers wiring those into a coherent gate.

Conclusion

The Node Readiness Controller is a gateway to a more robust, intentional scheduling model. By moving beyond the assumption that a Kubernetes Ready node is ready for your workloads, you can enforce the precise prerequisites your applications demand. Whether it’s certified hardware, validated storage, verified security controls, or licensed software, custom readiness conditions allow your platform to make smarter scheduling decisions.

This shifts the responsibility from the application developer (who must write complex initialization and error-handling logic) to the platform (which guarantees a capable environment). The result is fewer runtime surprises, more consistent performance, and a cluster that truly aligns with your operational and compliance requirements. Start by identifying one critical, non-standard dependency in your most important workloads, and implement a readiness gate for it. The pattern will quickly prove its value across your entire infrastructure.

Kubernetes Liveness Probe Anti-Patterns: Preventing Cascading Failures

Kubernetes Liveness Probe Anti-Patterns: Preventing Cascading Failures

If you manage production Kubernetes workloads, you’ve likely configured liveness probes. They’re a core Kubernetes feature designed to restart unhealthy containers, a seemingly straightforward mechanism for improving application resilience. Yet, as many seasoned engineers have learned the hard way, this tool can become a weapon of mass destruction against your own cluster’s stability. A misconfigured probe doesn’t just fail to heal an application—it can actively orchestrate its demise through cascading restarts, amplifying a transient issue into a full-blown outage.

Related reading: custom node readiness checks.

Related reading: HPA best practices.

This paradox is well-known in the community. As one engineer succinctly put it on Reddit, liveness probes are a feature that “looked great on paper but turned out to be a footgun” because they can easily “cause cascading failures when they get it wrong.” The problem isn’t the probe itself, but how we implement it. The default mindset of “just add a liveness probe” without deep consideration of application behavior and failure modes is a critical anti-pattern in platform engineering.

In this article, we’ll move beyond the basic documentation and dissect the common liveness probe anti-patterns that undermine system resilience. More importantly, we’ll provide a practical blueprint for designing health checks that act as a true safety net, not a tripwire.

Understanding the Probe Machinery: Liveness vs. Readiness

Before diagnosing anti-patterns, we must clearly distinguish between the two primary health checks. Confusing them is the first and most fundamental mistake.

  • Liveness Probe: Answers “Is the container process running?” A failed liveness probe results in the kubelet killing and restarting the container. Its purpose is to recover from a deadlock or a process that is running but unable to make progress.
  • Readiness Probe: Answers “Is the container ready to serve traffic?” A failed readiness probe causes the container to be removed from Service endpoints. It does not restart the pod. Its purpose is to handle temporary unavailability during startup, heavy load, or dependency failures.

The critical distinction is consequence: liveness restarts, readiness isolates. Using a liveness probe for a condition that should merely take a pod out of rotation is a guaranteed way to create instability.

Common Liveness Probe Anti-Patterns

These patterns are observed repeatedly in production incidents and post-mortems. Recognizing them is the first step toward remediation.

1. The “Everything is Liveness” Anti-Pattern

This is the most prevalent issue. Engineers point the liveness probe at the same endpoint as the readiness probe, or at a general health check that validates dependencies (databases, caches, message brokers).

Why it’s dangerous: If your database experiences a transient network blip, your liveness probe starts failing. Kubernetes dutifully restarts your pods. Now, instead of a few pods temporarily marked “not ready,” you have all your pods simultaneously crashing and restarting. This creates a thundering herd of new connections when they come back up, often overwhelming the recovering dependency and creating a failure cycle. The minor dependency hiccup has now become a total application outage.

2. The Overly Sensitive (Aggressive) Probe

This involves setting extremely tight timeouts (timeoutSeconds: 1) and short failure thresholds (failureThreshold: 2) on a probe that performs non-trivial work (e.g., a complex database query or an external API call).

Why it’s dangerous: Under normal system load (GC pauses, CPU contention, network latency spikes), the probe may occasionally exceed its strict timeout. Kubernetes interprets this as a failure. A couple of these transient delays in quick succession trigger a restart. You now have perfectly healthy pods being killed because your health check was more fragile than the application logic it was monitoring.

3. The Shared Fate Probe Endpoint

The probe endpoint shares the same thread pool, connection pool, or resource limits as the main application service. Under high load, the health check requests themselves can exhaust these resources, causing the probe to fail and triggering a restart on an overloaded pod—making the load situation worse for the remaining pods.

Why it’s dangerous: It creates a self-reinforcing failure mode. Load increases → probe resources are starved → probes fail → pods restart → load redistributes to fewer pods → load on remaining pods increases further. This cascade can quickly take down the entire service.

4. The Liveness Probe as a Readiness Gate

Using the liveness probe to prevent a pod from receiving traffic until it’s “fully ready,” often by setting an initial delay (initialDelaySeconds) that’s guessed rather than measured.

Why it’s dangerous: If the application takes longer to initialize than the initialDelaySeconds (due to a cold cache, large data load, etc.), the liveness probe will start failing immediately after the delay. The pod will be stuck in a crash loop (CrashLoopBackOff) before it ever had a chance to become ready. The correct tool for this job is the startupProbe.

A Blueprint for Resilient Liveness Probe Design

Designing a robust liveness probe requires a shift in philosophy. The probe should check for unrecoverable process failure, not general health. It should be minimally invasive, highly stable, and tolerant of transient issues.

Principle 1: Liveness Checks Must Be Local and Cheap

The liveness probe should check the state of the process itself, not its external dependencies. It should:

  • Run in-memory, without network calls (to other pods or external services).
  • Use minimal CPU and no blocking I/O.
  • Check an internal flag or a very simple, cached piece of internal state.

A classic example is a thread that updates a “last loop iteration” timestamp in shared memory. The liveness probe checks this timestamp. If it hasn’t been updated in X seconds, the main loop is likely deadlocked, and a restart is justified.

Principle 2: Configure Conservative Timeouts and Thresholds

Your probe configuration should account for the “noisy neighbor” reality of shared infrastructure. Use values that allow for occasional GC pauses and network jitter.

Here is a sample configuration reflecting conservative, production-oriented values:

livenessProbe:
  httpGet:
    path: /internal/health/liveness
    port: 8080
  initialDelaySeconds: 10  # Let the process settle
  periodSeconds: 10        # Don't check too frequently
  timeoutSeconds: 3        # Give it time to respond
  successThreshold: 1
  failureThreshold: 3      # Require multiple consecutive failures

Notice the failureThreshold: 3. A single failed probe means nothing. Two could be a coincidence. Three consecutive failures over 30 seconds is a much stronger signal of a real problem. This grace period can prevent countless unnecessary restarts.

Principle 3: Use the Startup Probe for Lengthy Initialization

For applications with slow boot times (Java VMs, apps loading large models), the startupProbe is your best friend. It disables the liveness and readiness checks until the app is up.

startupProbe:
  httpGet:
    path: /health/startup
    port: 8080
  failureThreshold: 30     # Try many times
  periodSeconds: 5         # Check every 5 seconds
# Liveness probe only starts working AFTER startup succeeds
livenessProbe:
  httpGet:
    path: /internal/health/liveness
    port: 8080
  initialDelaySeconds: 0   # No need for an extra delay
  periodSeconds: 10
  failureThreshold: 3

This configuration allows up to 150 seconds (30 * 5) for the application to start before it’s considered failed, while protecting it from being restarted during that sensitive boot period.

Principle 4: Isolate the Probe Endpoint

Ensure the endpoint used for the liveness probe:

  • Has a dedicated, minimal thread pool or runs outside the main request handling framework.
  • Is not subject to the same rate limiting or authentication as public APIs.
  • Returns a static, pre-computed response or checks only in-memory state.

This isolation ensures that application traffic cannot directly cause a liveness failure.

Putting It All Together: A Resilience-First Strategy

Your health check strategy should be a layered defense:

  1. Startup Probe: Guards the initialization phase. Allows slow starts without restarts.
  2. Readiness Probe: The primary traffic gatekeeper. Checks app health + critical dependencies. Fails fast and removes the pod from rotation under load or during dependency issues.
  3. Liveness Probe: The last resort. Checks only for internal process deadlock. Configured to be slow-triggering (high failureThreshold) and stable.

This strategy ensures that pods are restarted only when there is a high-confidence signal of an unrecoverable internal fault. All other issues—slow dependencies, high load, temporary errors—are handled gracefully by the readiness probe, which isolates the pod without triggering a potentially destabilizing restart.

Frequently Asked Questions

What is the difference between liveness and readiness probes?

A failed readiness probe removes the pod from Service endpoints — it stops receiving traffic but keeps running. A failed liveness probe restarts the container. Readiness answers “can this pod serve right now?”; liveness answers “is this process beyond recovery?”. Confusing the two is the root of most probe incidents.

Should every container have a liveness probe?

No. The default should be no liveness probe unless the process has a known failure mode that a restart genuinely fixes — a deadlock, a wedged event loop. A process that crashes on fatal errors already gets restarted by the kubelet; a liveness probe on top only adds a new way to kill healthy pods under load.

Why do my pods restart in a loop under high load?

Almost always a liveness probe timing out precisely because the pod is busy: load rises, the probe endpoint responds slowly, the kubelet kills the container, remaining pods absorb more load, and the cascade spreads. Fixes: generous timeoutSeconds and failureThreshold, a probe endpoint that does no real work, and moving traffic concerns to the readiness probe.

Should a liveness probe check database or downstream dependencies?

Never. If the database is down, restarting your pod does not fix the database — it just turns one outage into two. Liveness must check only process-internal health; dependency health belongs (with care) in readiness, and even there prefer degrading gracefully over dropping out of the Service.

Conclusion: From Footgun to Safety Mechanism

Liveness probes are not a “set and forget” feature. Treating them as such invites the cascading failures that give them a bad reputation. The goal is not to avoid liveness probes, but to implement them with the same care and production rigor as your application code.

By adhering to the blueprint above—keeping checks local and cheap, using conservative thresholds, leveraging startup probes, and isolating endpoints—you transform the liveness probe from a potential source of instability into a genuine resilience mechanism. It becomes a targeted surgical tool for recovering from true process deadlocks, while the readiness probe handles the broader spectrum of application health. In the complex, distributed environment of Kubernetes, this precise separation of concerns is not just a best practice; it’s a fundamental requirement for stable, resilient operations.

Review your probe configurations today. Ask the critical question: “Is this checking for a dead process, or just a busy one?” The answer will determine whether your health checks are preventing outages or causing them.

Headlamp vs FreeLens vs Lens (2026): Which Kubernetes UI Should You Standardise On?

There are two separate stories converging on the same decision. One is the Lens lineage: a commercial desktop IDE, an open core that got abandoned, and a community fork keeping it alive. The other is the Kubernetes Dashboard being archived and the community needing a replacement it actually governs.

Those stories used to have different answers. In 2026 they do not, because Headlamp has matured into something that competes directly with the Lens family rather than sitting in the “browser dashboard” box next to it. If you are choosing one Kubernetes UI to standardise a team on, these three are the shortlist.

This is not a feature beauty contest. The differences that will actually affect you are governance, deployment model and plugin ecosystem — and they point in different directions depending on whether you are equipping five engineers or five hundred.

What Is Headlamp?

Headlamp is an extensible Kubernetes UI developed under Kubernetes SIG UI, licensed Apache-2.0, and hosted as a CNCF Sandbox project. It was originally built by Kinvolk (since acquired by Microsoft) and now lives in the kubernetes-sigs GitHub organisation — the same org that holds Karpenter, kustomize and the cluster-api projects. With the original Kubernetes Dashboard archived and no longer receiving security fixes, Headlamp is where the community effort went.

The current release is v0.43.0 (June 2026), and the project ships feature releases roughly monthly.

The thing that makes Headlamp structurally different from the Lens family is that it runs in two modes from a single codebase:

  • As a desktop app, reading your local kubeconfig, exactly like Lens or FreeLens. Available for macOS, Windows (including ARM64 builds) and Linux.
  • As an in-cluster web UI, deployed with Helm and shared by the whole team, with access governed by Kubernetes RBAC rather than by whatever each engineer has in ~/.kube/config.

That second mode is the one that decides most enterprise evaluations, and neither Lens nor FreeLens offers it.

How to Install Headlamp

For the desktop app:

# macOS
brew install --cask headlamp

Windows and Linux builds are on the GitHub releases page. Note that the desktop binaries are unsigned, so macOS and Windows will warn you on first launch — an annoyance worth knowing about before you send installation instructions to twenty people.

For the in-cluster deployment:

helm repo add headlamp https://kubernetes-sigs.github.io/headlamp/
helm install headlamp headlamp/headlamp --namespace kube-system

Authentication in cluster mode is token-based. You create a ServiceAccount, bind it to a role, and users authenticate with its token; Headlamp then relies on standard Kubernetes RBAC to decide what each user may see and do. OIDC is supported for teams that want to wire it into an existing identity provider, and v0.43 added ServiceAccount token authentication for deployments sitting behind an auth proxy.

The practical consequence is worth spelling out: in cluster mode, what a user can do in the UI is exactly what their RBAC allows — no more, no less. There is no separate permission model to keep in sync, and no engineer walking around with a cluster-admin kubeconfig on a laptop because the tool needed it.

Headlamp vs FreeLens vs Lens: The Short Answer

Choose Headlamp if you want a UI your organisation governs rather than rents, or if you need a shared web UI with RBAC-backed access. It is the safest institutional answer, it is CNCF-hosted, and it is the natural landing spot if you arrived here because the Kubernetes Dashboard was retired.

Choose FreeLens if you want the classic Lens desktop experience, free and MIT-licensed, with the richest desktop plugin ecosystem of the three. It is the best individual-engineer tool and the correct migration target if you are still on OpenLens.

Choose Lens Desktop if your organisation already pays for it and values vendor support and the commercial feature set, and the licensing terms are settled. For most teams evaluating from scratch in 2026, it is the hardest of the three to justify.

If you want the full history of how Lens forked into OpenLens and then FreeLens — including the licensing detail that pushed most teams off Lens — that is covered in depth in FreeLens vs OpenLens vs Lens. And if you are specifically replacing the retired Dashboard with a browser-based option, Kubernetes Dashboard alternatives in 2026 covers the wider field.

Side-by-Side Comparison

HeadlampFreeLensLens Desktop
LicenceApache-2.0MITCommercial (Mirantis)
GovernanceCNCF Sandbox · Kubernetes SIG UICommunity projectSingle vendor
Current releasev0.43.0 (Jun 2026)v1.10.3 (Jul 2026)Rolling, commercial
Desktop app✅ macOS, Windows (incl. ARM64), Linux✅ macOS, Windows, Linux (amd64 + arm64)
In-cluster web UI✅ via Helm
Access controlKubernetes RBAC (+ OIDC)Whatever the local kubeconfig grantsLocal kubeconfig + vendor account
Account requiredNoNoYes
ExtensibilityPlugin systemExtension ecosystem (Flux, Gateway API, Karpenter…)Extension catalogue
CostFreeFreePaid for commercial use
Best forTeams and organisationsIndividual engineers and small teamsExisting paying customers

Where Headlamp Wins

Governance is the real argument. Everything else on this list is a feature that could be matched next quarter; who controls the project cannot be. Headlamp sits in the Kubernetes SIG structure under a CNCF Sandbox umbrella, which means the licence cannot be changed out from under you by a vendor with a new monetisation strategy. Teams that lived through the Lens licence change tend to weight this heavily, and they are right to.

Shared deployment with real access control. The in-cluster mode is genuinely differentiating. Instead of every engineer holding a kubeconfig with broad permissions, you deploy Headlamp once, wire it to your identity provider, and let RBAC decide what each person sees. For a platform team supporting dozens of developers, this converts “who has access to production?” from an unanswerable question into a kubectl query.

A steady, practical release cadence. v0.43 alone added a dedicated Job details view with integrated logs, a deployment creation form, batch scaling across multiple workloads, dry-run previews for rollbacks and manifest validation, and a diagnostics panel that surfaces troubleshooting hints directly on pod and workload pages. There is also ClusterProfile discovery through the Cluster Inventory API, still alpha, aimed at multi-cluster fleets — plus internationalisation work including right-to-left layouts for Arabic, Hebrew and Urdu, which very few tools in this space bother with.

Where FreeLens Wins

The desktop experience and the plugin ecosystem. FreeLens inherits years of Lens UX polish, and its extension catalogue is the most useful of the three for day-to-day platform work: a well-adopted FluxCD extension, Gateway API views, a Karpenter extension for inspecting NodePools, plus Kamaji and Sveltos integrations. If your workflow depends on a specific extension, check availability before switching — this is the category where Headlamp is furthest behind.

Zero friction to start. brew install --cask freelens, open it, your clusters are there. No Helm release, no ServiceAccount, no RBAC design session. For an individual engineer or a team of five, that matters more than governance does.

It is the OpenLens migration path. If you are still running OpenLens, you are running unpatched Electron and unpatched dependencies. FreeLens is the direct continuation of that codebase, so contexts and most extensions carry over.

Where Lens Desktop Still Makes Sense

Honestly: mostly when you already pay for it. If Lens is embedded in your organisation, the commercial support relationship has value, and the licence question has been resolved by someone with authority to resolve it, there is no urgency to move. Lens remains a polished product with a real company behind it.

What is hard to justify in 2026 is choosing it fresh. You would be adopting a single-vendor tool with an account requirement and a licence that has already changed once in a way that surprised its users, when two credible free alternatives exist — one of them CNCF-governed.

Plugins and Extensions: The Honest Comparison

Both Headlamp and FreeLens are extensible, but they are not equally extended.

FreeLens has the deeper catalogue for cluster-operations work today, largely because it inherited the Lens extension API and much of the OpenLens ecosystem ported across with modest changes.

Headlamp’s plugin system is well-designed and its plugins are first-class in both desktop and in-cluster modes — a plugin you deploy in cluster mode is available to everyone using that instance, which is architecturally nicer than asking each engineer to install an extension locally. But the catalogue is younger.

The practical test: list the three extensions you actually use, then check whether Headlamp has equivalents. If it does, governance should decide. If it does not, that is a legitimate reason to stay on FreeLens for now.

Decision Framework

Start here:
│
├── Do you need a shared UI with RBAC-backed access control?
│   ├── YES → Headlamp (in-cluster mode) — the only one that does this
│   └── NO ↓
│
├── Is vendor-neutral governance a hard requirement?
│   ├── YES → Headlamp
│   └── NO ↓
│
├── Do you depend on a specific Lens/OpenLens extension?
│   ├── YES → FreeLens (verify the extension exists first)
│   └── NO ↓
│
├── Are you already paying for Lens and happy with it?
│   ├── YES → Stay on Lens; revisit at renewal
│   └── NO ↓
│
└── Default → FreeLens for individuals, Headlamp for teams

For many organisations the honest answer is both: Headlamp deployed in-cluster as the shared, governed, RBAC-controlled view, and FreeLens on individual laptops for engineers who want the desktop workflow and the extensions. They are not mutually exclusive, and unlike running two alerting systems, there is no state to keep in sync — both are read-write clients against the same API server.

Frequently Asked Questions

Is Headlamp a replacement for the Kubernetes Dashboard?

Effectively yes. The original Kubernetes Dashboard has been archived and no longer receives security updates, bug fixes or new features. Headlamp is developed under Kubernetes SIG UI in the kubernetes-sigs organisation and is where the community effort went. It covers the same ground as the Dashboard — a web UI deployed into the cluster, access governed by RBAC — while adding a plugin system and an optional desktop mode the Dashboard never had.

Is Headlamp free?

Yes. Headlamp is licensed Apache-2.0 and hosted as a CNCF Sandbox project, so there is no commercial tier, no account requirement and no per-seat cost. That is one of the main practical differences against Lens Desktop, which requires an account and a commercial licence for business use.

Can Headlamp run as a desktop app like Lens?

Yes. Headlamp ships desktop builds for macOS, Windows (including ARM64) and Linux that read your local kubeconfig, exactly like Lens or FreeLens. It can also be deployed in-cluster with Helm as a shared web UI — the same codebase covers both modes. Note that the desktop binaries are unsigned, so macOS and Windows show a warning on first launch.

Headlamp vs FreeLens: which one should a team use?

For a team, Headlamp is usually the better institutional choice, because its in-cluster mode lets you deploy one shared UI whose permissions come from Kubernetes RBAC rather than from each engineer’s local kubeconfig. FreeLens is desktop-only. FreeLens wins on desktop polish and has the richer extension catalogue — FluxCD, Gateway API, Karpenter and others — so if your workflow depends on a specific extension, verify Headlamp has an equivalent before switching. Many organisations run both.

Is OpenLens still safe to use in 2026?

No. OpenLens is no longer maintained and community builds stopped tracking upstream, which means you are running unpatched Electron and unpatched dependencies. The direct migration path is FreeLens, which continues the same codebase under the MIT licence, so your kubeconfig, contexts and most extensions carry over.

Conclusion

If you are equipping a team and starting fresh, Headlamp is the defensible default in 2026. Not because it wins on features — FreeLens matches or beats it on desktop polish and extensions — but because it is the only one of the three where the deployment model fits how organisations actually control access, and the only one whose governance guarantees the licence will not move.

If you are one engineer who wants the best desktop Kubernetes IDE and does not care who owns the project, install FreeLens and get on with your day.

The one answer that is wrong in 2026 is staying on OpenLens.

Related reading: FreeLens extensions: the complete catalogue and how to install them.

Using ~ (null) in Helm: Deleting Default Values and Handling Optional Fields

Using ~ (null) in Helm: Deleting Default Values and Handling Optional Fields

You override a chart’s default livenessProbe with an exec command, deploy, and Kubernetes rejects it: “may not specify more than one handler type.” The chart’s default httpGet probe is still there, merged underneath your override, and now the pod spec has two probe handlers. You didn’t add it. You can’t see it in your values file. And the fix is a single character: ~.

That ~ is YAML’s null, and in Helm it does something most people never learn: setting a key to null deletes it from the merged values entirely, instead of setting it to a null value. It’s the cleanest way to remove a default a chart baked in — and it’s the tip of a whole set of null-handling behaviors (absent vs null vs empty, --set foo=null, default, required, hasKey) that quietly decide whether your templates render valid YAML or foo: <no value>.

This guide covers the null-deletes-a-key trick in depth, where it works and where it bites (the --reuse-values trap, the Helm 4 regression), and the related functions you need to tell “the user didn’t set this” apart from “the user set this to empty.” Everything is verified against current Helm docs and behavior.

First: ~ is just YAML null

Before Helm, this is pure YAML. All of these mean the same thing — null:

a: ~        # canonical shorthand
b: null
c: Null
d: NULL
e:          # empty value is also null

~ is the canonical short form in the YAML spec; null/Null/NULL/empty are equivalent spellings. In a Helm values.yaml, writing foo: ~ is identical to foo: null. People reach for ~ because it’s terse and unmistakable — an empty value (foo:) is easy to misread as “I forgot to fill this in,” whereas foo: ~ reads as a deliberate null.

The Killer Trick: null Deletes a Default Key

Here is the behavior that makes ~ worth an article. When you override a chart’s values — with a -f values file, a parent chart overriding a subchart, or --set — Helm merges your values on top of the chart’s defaults. A normal override replaces a value. But a null override is special: Helm removes the key from the result.

Straight from the Helm docs (Chart Template Guide → Values Files):

“If you need to delete a key from the default values, you may override the value of the key to be null, in which case Helm will remove the key from the overridden values merge.”

The canonical example is the liveness-probe foot-gun from the intro. Say the chart defaults to:

# chart's values.yaml
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080

You want an exec probe instead. If you just add your exec, the merge keeps the default httpGet — and a probe with both exec and httpGet is invalid. You delete the default with null:

# your override values.yaml
livenessProbe:
  httpGet: ~          # ← deletes the chart's default httpGet
  exec:
    command: [cat, docroot/CHANGELOG.txt]

Or on the command line, exactly as the Helm docs show it:

helm install stable/drupal \
  --set livenessProbe.exec.command='{cat,docroot/CHANGELOG.txt}' \
  --set livenessProbe.httpGet=null

The result has only the exec handler. Without the httpGet=null, both survive and Kubernetes rejects the manifest.

Why it works: Helm’s value coalescing walks the override tree onto the defaults. A present key with a real value overwrites; a present key with null is treated as an instruction to remove. This is the only way to subtract from a chart’s defaults — there’s no --unset flag (more on that below).

Where the null-delete works

The same mechanism applies anywhere Helm coalesces values:

  • A -f override file on top of the chart’s values.yaml (the docs example above).
  • A parent chart overriding a subchart. In the parent’s values.yaml, nulling a key under the subchart’s name deletes that subchart default:
  • “`yaml
  • # parent values.yaml
  • mysubchart:
  • someDefault: ~ # remove a default the subchart shipped
  • “`
  • --set key=null at install/upgrade time.

--set foo=null vs --set-string foo=null

On the CLI the distinction matters, and it’s a common source of “why didn’t it delete?”:

CommandResult
--set foo=nullfoo becomes a real nil → key is deleted
--set-string foo=nullfoo becomes the literal string "null" (no deletion)
--set a=null,name=[]a: null, name: []

Helm’s --set parser does type conversion: the literal null (case-insensitive) is converted to a Go nil, which triggers the delete. --set-string forces everything to stay a string, so null is just the four-character word "null" — which is almost never what you want here. If your deletion isn’t happening, check you didn’t reach for --set-string.

Version Support (and the Helm 4 warning)

This is where “desde qué versión” gets interesting:

  • Helm 2 introduced deleting a key by setting it to null, but only reliably for top-level keys. Nested null-deletion (like web.livenessProbe.httpGet: null) was buggy — it could emit Cannot overwrite table item ... with non table value and override with null instead of deleting.
  • Helm 3 is where the documented behavior works as advertised, top-level and nested. If you’re on Helm 3, everything above is solid.
  • ⚠️ Helm 4 — currently a regression. As of a still-open issue (filed March 2026), Helm 4 no longer reliably deletes keys via null: both foo: (blank) and foo: null can fail schema validation with errors like Invalid value: "null": ... must be of type string, especially against strict Kubernetes 1.34+ schemas. If you’ve moved to Helm 4, test null-deletion before relying on it — the behavior that was rock-solid in Helm 3 is in flux. This interacts directly with values JSON schema validation: a schema that types a field as string will reject a null, so schema and null-deletion can fight each other.

The --reuse-values Trap

The one place null-deletion does not do what you’d hope: helm upgrade --reuse-values.

--set foo=null deletes a key relative to the chart’s defaults. It does not cleanly “un-set” a value you previously set explicitly and are now carrying forward with --reuse-values — it overrides it with null rather than falling back to the chart default. There is a long-standing feature request for an explicit --unset flag precisely because this case has no clean answer today.

Practical rule: to genuinely reset a value back to the chart default on upgrade, prefer re-specifying your full intended values (-f) over leaning on --reuse-values plus --set x=null.

Absent vs null vs empty: The Trio That Trips Everyone

Deleting keys is half the story. The other half is reading optional values in templates — and Helm/Sprig blur three states that feel different: key absent, key present but null, and key present but empty (0, "", [], {}, false).

The critical thing to internalize: default, required, empty, and coalesce all treat nil, 0, "", empty list/map, and false as the same “empty.” They cannot tell “unset” from “set to zero.”

replicas: 0        # a DELIBERATE zero...
```
```gotemplate
{{ .Values.replicas | default 3 }}   # ...renders 3, not 0 — surprise!

Here’s what each tool actually does:

You want to…UseBehavior
Provide a fallback for empty/unset`{{ .Values.foo \default “x” }}`Returns "x" if foo is nil, 0, "", [], {}, or false
Fail loudly if unset{{ required "foo is required" .Values.foo }}Errors on nil and on empty string (same “empty” rule as default)
First non-empty of several{{ coalesce .Values.a .Values.b "x" }}Skips every empty/null, returns first real value
Tell present-but-null from absent{{ if hasKey .Values "foo" }}The only reliable presence check — true even when the value is null
Safely read a nested optional{{ dig "a" "b" "fallback" .Values }}Walks .a.b, returns "fallback" if any level is missing
Branch on a condition{{ ternary "yes" "no" .Values.enabled }}"yes" if truthy, "no" if empty/false

If you need to honor a deliberate 0 or false, default is wrong — use hasKey to check presence explicitly:

replicas: {{ if hasKey .Values "replicas" }}{{ .Values.replicas }}{{ else }}3{{ end }}

The Rendering Foot-gun: <no value> vs null vs ""

The nastiest null bug isn’t logic — it’s a null leaking into your YAML as a broken string. The same nil value renders three different ways:

foo: {{ .Values.foo }}            # → foo: <no value>   ❌ invalid YAML-ish garbage
foo: {{ .Values.foo | toYaml }}   # → foo: null         ✅ valid YAML null
foo: {{ .Values.foo | quote }}    # → foo: ""           ✅ valid empty string

Bare-printing an unset value gives you the literal text <no value> in the manifest — which is not null, not empty, just a string that will confuse Kubernetes or your reader. (You may also see <nil> in some contexts; the exact literal is a Go-template detail that has shifted across Helm 3 minor versions, so don’t hard-code assumptions about which one appears — the point is it’s not what you want.)

The fixes:

  • Piping through toYaml turns nil into a proper null — use it for whole objects/maps: {{ .Values.config | toYaml | nindent 2 }}.
  • Piping through quote turns nil into "" — use it for optional scalars that must be strings.
  • Best of all, skip the key entirely when empty with with:
  • “`gotemplate
  • {{- with .Values.foo }}
  • foo: {{ . | quote }}
  • {{- end }}
  • “`
  • The with block is skipped for any empty/nil value, so an unset foo produces no line at all — the idiomatic Helm “omitempty.”

Switching Off a Subchart Block with null

One more practical use of ~. Because nil is “empty,” setting a value to null makes an {{ if }} guard fall through:

# subchart default turns something on
metrics:
  serviceMonitor:
    enabled: true
```
```yaml
# parent override switches it off cleanly
metrics:
  serviceMonitor: ~     # or: enabled: false

Nulling the whole block (or the flag) makes {{ if .Values.metrics.serviceMonitor }} evaluate false — a tidy way for a parent chart or an environment override to disable a section a subchart enabled by default, using the same empty-semantics as everything above.

Cheat Sheet

GoalSyntax
Write a null in valuesfoo: ~ (or foo: null)
Delete a chart’s default keyoverride it with ~ / null
Delete via CLI--set foo=null (not --set-string)
Fallback for empty/unset`{{ .Values.foo \default “x” }}`
Distinguish null from absent{{ if hasKey .Values "foo" }}
Require a value{{ required "msg" .Values.foo }}
Nested optional read{{ dig "a" "b" "fallback" .Values }}
Null-safe object into YAML`{{ .Values.obj \toYaml \nindent 2 }}`
Omit a key when empty{{- with .Values.foo }} … {{- end }}

Wrapping Up

~ in Helm is a two-job character: it writes a YAML null, and — the part almost nobody documents in their own charts — it deletes a default key from the merged values, which is the only clean way to subtract a probe, an annotation, or a whole block that a chart shipped by default. Around it sits a set of null rules worth memorizing: default/required/empty can’t tell unset from zero (use hasKey when that matters), and an unset value bare-printed becomes <no value> unless you route it through toYaml, quote, or with.

Two warnings to carry: --reuse-values doesn’t cleanly un-set values, and Helm 4’s null-deletion is currently a regression — so if you’re on 4.x, verify before you rely on it.

For more Helm depth, see the companion guides on values JSON schema validation (which interacts directly with null handling), loading external files into ConfigMaps and Secrets, and what’s new in Helm 4.

Sources

GPU scheduling on Kubernetes with Dynamic Resource Allocation (DRA): the 2026 guide

GPU scheduling on Kubernetes with Dynamic Resource Allocation (DRA): the 2026 guide

GPU scheduling in Kubernetes used to be deceptively simple: install the NVIDIA device plugin, request nvidia.com/gpu: 1, and let the default scheduler find a node with one available GPU. That model got many clusters into production, but it encoded the wrong abstraction. A modern GPU is not just an integer. It has memory size, architecture, interconnect, topology, partitioning modes, sharing modes, and health state.

Related reading: Cluster Autoscaler vs Karpenter.

Dynamic Resource Allocation (DRA) is Kubernetes’ answer to that mismatch. The core DRA APIs graduated to GA in Kubernetes 1.34, with the stable resource.k8s.io/v1 API enabled by default. In 2026, this matters because the ecosystem around AI infrastructure has also moved: NVIDIA donated its DRA Driver for GPUs to the Kubernetes community under CNCF governance at KubeCon Europe 2026, Kueue has native concepts for DRA-aware quota management, KAI Scheduler is a CNCF Sandbox project for large GPU fleets, and inference stacks such as vLLM and KServe are becoming the runtime layer above the scheduler.

This is not a “replace one YAML key with another” migration. DRA changes where device knowledge lives. Instead of asking Kubernetes for a count of opaque extended resources, workloads request a claim against a class of devices, and the scheduler allocates a concrete device that satisfies the claim.

Why the device-plugin model is reaching its limits

The Kubernetes device plugin framework exposes vendor devices to the kubelet. For NVIDIA GPUs, the traditional resource name is nvidia.com/gpu, requested in container resources.requests and resources.limits. This remains useful, especially for simple clusters.

The limitation is in the API shape. Kubernetes extended resources are integer resources and cannot be overcommitted. The Kubernetes documentation also states that devices cannot be shared between containers through the basic extended-resource model. That is fine for “one pod owns one whole GPU”. It is much weaker for LLM inference, mixed training queues, fractional capacity, MIG profiles, topology-sensitive multi-GPU jobs, and heterogeneous node pools.

The device-plugin model also pushes too much meaning into out-of-band policy. If you need A100s rather than L4s, you usually add node labels, node affinity, taints, or separate node groups. If you need a MIG slice, you configure GPU Operator and device plugin strategy, then expose separate resource names or labels. If you need low-latency multi-GPU placement, you combine scheduler plugins, topology labels, and workload-specific conventions.

Those workarounds fragment the source of truth. The scheduler sees integer capacity. The driver knows device details. The autoscaler knows node templates. The ML platform knows model requirements. DRA gives Kubernetes a structured device model so those systems can coordinate through API objects.

For node provisioning, this does not remove the need for good autoscaling. You still need a node autoscaler that can bring up GPU capacity with the right labels, taints, AMI, driver stack, and instance family. If you run on AWS, /cluster-autoscaler-vs-karpenter/ is directly relevant: GPU pods sitting Pending are often a node provisioning problem before they are a scheduler problem. On EKS, /eks-auto-mode/ is also worth reading because managed node lifecycle and accelerator support change how much of the stack you own.

What DRA actually adds

DRA introduces Kubernetes APIs for claiming devices. The stable API group is resource.k8s.io/v1. The important objects are:

API objectScopePurpose
DeviceClassclusterDefines a category of devices and optional selectors/configuration. Claims reference a DeviceClass.
ResourceSliceclusterPublished by DRA drivers. Describes available devices, attributes, capacity, and node access.
ResourceClaimnamespaceRequests access to devices. The scheduler allocates concrete devices into the claim status.
ResourceClaimTemplatenamespaceTemplate for per-pod ResourceClaims, similar in spirit to volume claim templates.
Pod spec.resourceClaimspodMakes a ResourceClaim or ResourceClaimTemplate available to the pod.
Container resources.claimscontainerAttaches a named claim to a specific container.

The control flow is simple. A DRA driver publishes devices as ResourceSlice objects. A cluster administrator or driver provides DeviceClass objects. A workload creates a ResourceClaim or references a ResourceClaimTemplate. During scheduling, Kubernetes evaluates the claim against ResourceSlices, picks devices on nodes where the pod can run, stores the result in ResourceClaim status, and the driver prepares the device for the pod.

This is close to the PersistentVolumeClaim mental model: a pod references a claim with requirements, and Kubernetes binds it to something concrete. With DRA, the pod claims a device from a class, with selectors and constraints that drivers and the scheduler can reason about.

Do not overstate this: DRA is not live GPU hot-plugging, and it is not a model-serving platform. It is a scheduling-time allocation framework. You still need CPU and memory requests to be correct, as covered in /2026-05-kubernetes-resource-requests-limits/. You still need queueing for batch fairness and an inference runtime for serving.

A minimal DRA GPU request

The exact DeviceClass names and available attributes depend on the installed driver. NVIDIA’s NIM Operator documentation says the NVIDIA DRA driver deploys a default gpu.nvidia.com DeviceClass for physical GPUs. Always verify this in your cluster:

kubectl get deviceclasses
kubectl get resourceslices

For a single GPU per pod, use a ResourceClaimTemplate:

apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: one-nvidia-gpu
  namespace: ai
spec:
  spec:
    devices:
      requests:
      - name: gpu
        exactly:
          deviceClassName: gpu.nvidia.com
          allocationMode: ExactCount
          count: 1
---
apiVersion: batch/v1
kind: Job
metadata:
  name: dra-gpu-smoke-test
  namespace: ai
spec:
  completions: 1
  parallelism: 1
  template:
    spec:
      restartPolicy: Never
      resourceClaims:
      - name: gpu
        resourceClaimTemplateName: one-nvidia-gpu
      containers:
      - name: cuda
        image: nvcr.io/nvidia/cuda:12.5.1-base-ubuntu22.04
        command: ["nvidia-smi"]
        resources:
          claims:
          - name: gpu
          requests:
            cpu: "1"
            memory: 1Gi
          limits:
            memory: 1Gi

This manifest uses the stable resource.k8s.io/v1 API and the pod-level resourceClaims plus container-level resources.claims fields shown in the Kubernetes DRA task documentation. It deliberately does not use the legacy nvidia.com/gpu resource request.

In production, you will usually add selectors. Kubernetes supports CEL selectors in DRA claims, but attribute names are driver-specific. Inspect ResourceSlices before standardizing selectors:

kubectl get resourceslices -o yaml

For example, a platform team might publish DeviceClasses such as “inference-l4”, “training-h100”, or “mig-1g-10gb” instead of asking application teams to write low-level CEL expressions.

MIG, sharing, and topology awareness

MIG is where DRA becomes more than nicer syntax. With the device-plugin model, MIG support works, but the cluster often ends up with a mixture of resource names, node labels, and operational conventions. DRA lets the driver publish device shapes and capacities in ResourceSlices.

NVIDIA’s current DRA driver documentation is cautious: the driver manages GPUs and ComputeDomains; ComputeDomains are officially supported for robust and secure Multi-Node NVLink, while some GPU allocation features are still described as exploratory in the upstream README. NVIDIA’s NIM Operator documentation already covers Kubernetes v1.34 or later, resource.k8s.io/v1, the gpu.nvidia.com DeviceClass, full GPU versus MIG decisions, and the GPU Operator path. Pin driver versions and test the exact mode you intend to offer.

Topology matters at two levels.

First, there is node-local topology: NUMA locality, PCIe lanes, NVLink, and whether devices are close to the CPUs and memory the pod uses. Kubernetes has long had a Topology Manager in kubelet, but DRA gives the scheduler more structured information before placement.

Second, there is fleet topology: racks, blocks, zones, and inter-node fabric. Kueue’s Topology Aware Scheduling documentation targets AI/ML workloads where pod-to-pod bandwidth affects runtime and cost. DRA handles device allocation; Kueue decides when a workload should be admitted and how scarce quota should be shared.

Preemption belongs in that same layered view. Kubernetes scheduler preemption and Kueue preemption can make room for higher-priority workloads, but DRA itself is the device allocation substrate. In practice, you combine DRA claims, PriorityClasses, Kueue ClusterQueues, and possibly KAI Scheduler policies to get predictable multi-tenant behavior.

What NVIDIA’s CNCF donation changes

On March 24, 2026, NVIDIA announced at KubeCon Europe in Amsterdam that it was donating the NVIDIA DRA Driver for GPUs to CNCF, moving it from vendor governance to community ownership under the Kubernetes project.

It changes the risk profile for platform teams. A GPU DRA driver under Kubernetes community governance is easier to treat as part of the cloud-native substrate, and it creates a clearer collaboration point for cloud providers, Kubernetes SIGs, Kueue, KAI Scheduler, and inference platforms.

It does not make NVIDIA-specific hardware vendor-neutral. CUDA, MIG, NVLink, GPU Operator, and driver lifecycle remain NVIDIA concerns. What improves is the Kubernetes integration surface for advertising, claiming, allocating, and preparing devices.

NVIDIA also announced that KAI Scheduler had been onboarded as a CNCF Sandbox project. CNCF describes KAI Scheduler as a Kubernetes scheduler for optimizing GPU resource allocation for AI workloads in large-scale clusters. DRA models and allocates devices; KAI provides AI-focused scheduling policy; Kueue provides quota, admission, fair sharing, and preemption; KServe and vLLM provide the inference serving layer.

For production inference, vLLM and KServe are consumers of GPU scheduling, not replacements for it. vLLM documents KServe integration for distributed model serving, and KServe documents multi-node, multi-GPU inference using a vLLM serving runtime.

Migration: device plugin to DRA

Do not migrate the whole fleet in one step. A practical migration looks like this:

  1. Inventory existing GPU workloads. Classify them by device shape: whole GPU training, small inference, MIG-friendly inference, multi-GPU single-node, and multi-node training.
  2. Upgrade the control plane and nodes to Kubernetes 1.34 or later. Verify resource.k8s.io/v1 is available with kubectl api-resources | grep resource.k8s.io.
  3. Install or upgrade the GPU Operator and NVIDIA DRA driver in a small, isolated GPU node pool.
  4. Verify DeviceClass and ResourceSlice objects before writing application selectors.
  5. Define platform-owned DeviceClasses for common use cases. Prefer “h100-training”, “l4-inference”, or “mig-small” over asking every team to understand device internals.
  6. Convert one non-critical workload from nvidia.com/gpu to a ResourceClaimTemplate. Keep CPU and memory requests unchanged unless you are intentionally resizing the workload.
  7. Add Kueue for batch admission if multiple teams compete for GPUs. Use ClusterQueues, ResourceFlavors, cohorts, fair sharing, and preemption policies.
  8. Validate node provisioning. If the claim is valid but no node exists, the autoscaler still has to create the right GPU node.
  9. Roll into serving platforms. For KServe/vLLM, verify how the serving controller passes or creates DRA claims.
  10. Retire legacy paths only after observability, rollback, and quota policies are in place.

During migration, it is reasonable to run legacy device-plugin workloads and DRA workloads side by side on separate node pools. Avoid advertising the same physical GPU through two allocation systems to the same scheduling domain unless the driver documentation explicitly supports that configuration.

Device plugin vs DRA

CapabilityDevice plugin / extended resourceDRA
Workload requestnvidia.com/gpu: 1 integer resourceResourceClaim or ResourceClaimTemplate
API maturityDevice plugin framework exists since Kubernetes 1.10 betaCore DRA APIs GA in Kubernetes 1.34
Device attributesMostly external labels and conventionsStructured attributes/capacity through ResourceSlices
SharingLimited by extended-resource model; vendor-specific sharing modesClaims can model sharing patterns when supported by driver and feature set
MIG / partitionsWorks through vendor configuration and resource exposureBetter fit for requesting specific device shapes
Scheduler awarenessCounts resources on nodesAllocates concrete devices during scheduling
Autoscaler visibilityOften depends on node templates and resource namesStructured parameters improve simulation potential, but autoscaler support still matters
Best fitSimple whole-GPU workloadsHeterogeneous, partitioned, shared, or topology-sensitive GPU fleets

When you do not need DRA yet

DRA is not mandatory for every GPU cluster in 2026.

If every workload needs exactly one whole GPU on a homogeneous node pool, the device plugin model may be simpler. If your managed Kubernetes provider does not support the driver path you need, waiting is rational. If your bottleneck is cold node provisioning, image pull time, model download time, or bad CPU/memory requests, DRA will not fix that by itself.

Pilot DRA when multiple teams compete for expensive GPUs, you run mixed GPU models, MIG or sharing is a first-class requirement, multi-GPU topology affects performance, or you need cleaner integration between scheduling, quota, and AI workload platforms.

FAQ

Is DRA GA in Kubernetes?

The core DRA APIs graduated to GA in Kubernetes 1.34, using resource.k8s.io/v1 and enabled by default. Some surrounding features continued to mature later; for example, Kubernetes documentation in 2026 marks certain DRA task flows as v1.35 [stable] and DRA prioritized lists as v1.36 [stable].

Does DRA replace the NVIDIA device plugin?

For DRA workloads, the claim replaces the nvidia.com/gpu extended-resource request. Operationally, many clusters will run both models during migration. The NVIDIA DRA driver is the relevant component for DRA-based GPU allocation.

Can I request fractional GPUs with DRA?

DRA provides a framework for richer requests and sharing, but the exact behavior depends on Kubernetes feature maturity and the driver. NVIDIA documents MIG and time-slicing paths in its GPU stack, while DRA consumable capacity adds a Kubernetes model for sharing device capacity. Validate the exact mode you want before promising fractional GPU self-service.

Is DRA enough for multi-tenant GPU scheduling?

No. DRA allocates devices. For fair sharing, quota, admission, and preemption across teams, add Kueue or a scheduler layer such as KAI Scheduler, depending on your workload shape.

Does DRA help with inference?

Yes, but indirectly. DRA makes the GPU request more accurate. vLLM and KServe still handle serving concerns such as model runtime, scaling, routing, and multi-node inference patterns.

CTA: pilot DRA in one GPU node pool

The right first step is one GPU node pool. Install the NVIDIA DRA driver, verify DeviceClass and ResourceSlice objects, and convert one smoke-test Job plus one real low-risk workload to ResourceClaimTemplate. Measure scheduling latency, claim allocation status, GPU utilization, failure modes, and autoscaler behavior. If the pilot is boring, expand it to one tenant queue. If it is noisy, fix the platform contract first.

Sources

  • Kubernetes v1.34 DRA GA announcement: https://kubernetes.io/blog/2025/09/01/kubernetes-v1-34-dra-updates/
  • Kubernetes Dynamic Resource Allocation concepts: https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/
  • Kubernetes DRA workload task and YAML fields: https://kubernetes.io/docs/tasks/configure-pod-container/assign-resources/allocate-devices-dra/
  • Kubernetes ResourceClaim API reference: https://kubernetes.io/docs/reference/kubernetes-api/resource/resource-claim-v1/
  • Kubernetes ResourceSlice API reference: https://kubernetes.io/docs/reference/kubernetes-api/resource/resource-slice-v1/
  • Kubernetes device plugin documentation: https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/
  • Kubernetes DRA consumable capacity: https://kubernetes.io/blog/2025/09/18/kubernetes-v1-34-dra-consumable-capacity/
  • KEP-4381 DRA structured parameters: https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/4381-dra-structured-parameters
  • NVIDIA DRA Driver for GPUs repository: https://github.com/kubernetes-sigs/dra-driver-nvidia-gpu
  • NVIDIA KubeCon Europe 2026 DRA driver donation announcement: https://blogs.nvidia.com/blog/nvidia-at-kubecon-2026/
  • NVIDIA NIM Operator DRA support: https://docs.nvidia.com/nim-operator/latest/dra.html
  • NVIDIA GPU Operator sharing documentation: https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-sharing.html
  • CNCF KAI Scheduler project page: https://www.cncf.io/projects/kai-scheduler/
  • Kueue overview and DRA concepts: https://kueue.sigs.k8s.io/docs/overview/
  • Kueue Topology Aware Scheduling: https://kueue.sigs.k8s.io/docs/concepts/topology_aware_scheduling/
  • vLLM KServe integration: https://docs.vllm.ai/en/stable/deployment/integrations/kserve/
  • KServe multi-node/multi-GPU vLLM inference: https://kserve.github.io/website/docs/model-serving/generative-inference/multi-node

Talos Linux vs Bottlerocket vs Flatcar Container Linux: choosing an immutable OS for Kubernetes nodes (2026)

Talos Linux vs Bottlerocket vs Flatcar Container Linux: choosing an immutable OS for Kubernetes nodes (2026)

Kubernetes node operating systems have become a real design choice again. For years, most teams ran Ubuntu, Debian, RHEL, or Amazon Linux on workers, then layered kubelet, containerd, hardening, patching, observability agents, GPU drivers, and configuration management on top. That works, but it leaves a large mutable host underneath a scheduler that treats nodes as replaceable capacity.

Related reading: the full Talos Linux guide.

Immutable container-focused operating systems attack the problem differently. They reduce the host to a small, versioned artifact, make updates atomic, and move configuration into declarative APIs or boot-time documents. The goal is fewer moving parts, less drift, smaller attack surface, predictable upgrades, and replaceable nodes.

In 2026, the three names that come up most often are Talos Linux, Bottlerocket, and Flatcar Container Linux. They overlap, but they are not interchangeable:

  • Talos Linux is the most Kubernetes-specific and the most radical: no SSH, no shell, no package manager, API-only operations.
  • Bottlerocket is the AWS-native container host: strong fit for EKS and ECS, image variants, API settings, admin containers, and AWS support.
  • Flatcar Container Linux is the evolutionary successor to the CoreOS Container Linux model: read-only system partition, Ignition/Container Linux Config, systemd, SSH, automatic updates, and broad platform coverage.

For a step-by-step Talos implementation guide, read the deep dive: Talos Linux: The Immutable, API-Driven OS for Kubernetes. This article is a decision guide: what changes operationally, where each OS fits, and what trade-offs senior platform teams should care about.

The decision in one paragraph

Choose Bottlerocket when your platform is AWS-based and you want a supported node OS that integrates with EKS, EC2, ECS, AMIs, and AWS support plans. Choose Talos when you want a Kubernetes-first OS across bare metal, edge, private cloud, or multi-cloud, and can replace SSH-era habits with an API-driven model. Choose Flatcar when you want immutable infrastructure and automatic updates, but still need systemd, SSH, Ignition-style provisioning, and a smoother migration from Ubuntu or CoreOS-like patterns.

What Talos Linux is

Talos Linux is a Kubernetes-optimized Linux distribution from Sidero Labs. Its design is deliberately narrow: it exists to run Kubernetes. It is not a general-purpose server distribution with a hardened profile. The host has no SSH daemon, no interactive shell, and no package manager. Configuration and day-2 operations go through talosctl and an authenticated gRPC API using mutual TLS.

That changes the security model. On a traditional node, operational access usually means SSH, sudo, shell history, mutable filesystems, ad hoc commands, and sometimes a long tail of packages that were installed to debug one incident and never removed. Talos removes that path. You inspect resources, logs, services, disks, and Kubernetes component state through the Talos API. You change machine configuration by applying declarative config patches. You upgrade by asking the node to move to a new installer image.

The upside is a very small node surface. Sidero states that Talos ships with fewer than 50 OS binaries and no package manager; current releases also publish SBOM, VEX, and cryptographic signatures. Talos upgrades use an A/B image scheme so the previous kernel and OS image remain available for rollback. As of this review, the latest GitHub release is Talos v1.13.5, published on June 22, 2026.

The cost is operational discipline. If your incident response muscle memory is ssh, strace, edit a file, restart a daemon, Talos forces a redesign around API resources, Kubernetes-level observability, ephemeral debug containers where appropriate, and reproducible configuration.

What Bottlerocket is

Bottlerocket is an open-source Linux-based operating system purpose-built by Amazon Web Services for running containers. AWS provides Bottlerocket as no-cost AMIs for EC2 and documents it as a container host for EKS, ECS, VMware, and bare metal. The product center of gravity is clearly AWS.

Bottlerocket does not use a general-purpose package manager. Instead, it ships as variants: predefined images tailored to platform, architecture, orchestrator, orchestrator version, and sometimes flavor. Kubernetes variants are tied to Kubernetes minor versions, and Bottlerocket also has NVIDIA and FIPS flavors. The Bottlerocket OS version is separate from the Kubernetes variant version. As of this review, the current docs are under the 1.62.x branch and the latest GitHub release is v1.62.1, published on June 22, 2026.

Configuration is modeled and exposed through an API. Updates are image-based and use partition flips rather than package mutation. For Kubernetes clusters, the Bottlerocket Update Operator, or Brupop, coordinates in-place node updates. It runs a controller, node agents, and an API server, drains nodes through the eviction API, respects PodDisruptionBudgets, and rolls updates in waves.

Bottlerocket is less austere than Talos. It supports host and admin container concepts for controlled access, which helps with break-glass paths, SSM-style workflows, and AWS support expectations. The trade-off is portability: it can run outside EC2, but its strongest ecosystem, release consumption model, and support story are AWS-shaped.

What Flatcar Container Linux is

Flatcar Container Linux is a community Linux distribution for container workloads. It started as a fork of CoreOS Container Linux and now lives in the Cloud Native Computing Foundation. CNCF lists Flatcar as an Incubating project, accepted on August 2, 2024.

Flatcar is immutable in the classic Container Linux sense. The system partition is read-only, the OS is delivered as an image, updates are automatic and atomic, and the node is provisioned declaratively at boot. Unlike Talos, Flatcar keeps a familiar Linux model: systemd, SSH, files, units, and host-level customization. The current recommended provisioning path is Container Linux Config transpiled to Ignition. The older cloud-config model remains useful historically, but the docs recommend Container Linux Config for new provisioning.

Flatcar’s platform coverage is broad, including major clouds, OpenStack, Proxmox, VMware, KubeVirt, libvirt, QEMU, and VirtualBox. The release page currently shows Stable 4593.2.3, Beta 4694.1.0, Alpha 4722.0.0, and LTS 4081.3.8. The LTS model is useful for teams that want fewer feature changes.

Flatcar’s day-2 profile is closest to traditional Linux, which is both its strength and its risk. You can carry forward systemd-based automation, SSH debugging, and existing tools. You can also carry forward drift if you treat it like a mutable server.

Comparative table

AreaTalos LinuxBottlerocketFlatcar Container Linux
Core philosophyKubernetes-only, API-managed, no shellAWS-built container host with variantsContainer Linux successor with systemd and Ignition
Best fitBare metal, edge, private cloud, multi-cloud, high-security clustersEKS/ECS and AWS-heavy fleetsUbuntu/CoreOS-style migrations that still need Linux primitives
Management modeltalosctl over mTLS gRPC; declarative machine configAPI settings, variants, AMIs, BrupopContainer Linux Config/Ignition, systemd, SSH, channels, Nebraska
SSH and shellNo SSH, no shellNot a normal SSH-first OS; controlled access through host/admin containersSSH and normal Linux debugging patterns are available
Package managerNoneNone; use variants insteadNo traditional package workflow for the base OS; extend through containers, Ignition, systemd, and systemd-sysext
UpdatesAPI-triggered upgrades, A/B image scheme, rollback pathPartition-flip updates; Brupop for Kubernetes nodesAutomatic atomic updates via channels and reboot strategies
Kubernetes couplingVery high; control plane and workers are first-classHigh for Kubernetes variants; also ECSMedium; Kubernetes lifecycle is assembled above the OS
Attack surfaceSmallest and most opinionated; no SSH, no shell, no package managerSmall, container-focused, no package manager, AWS security postureMinimal container OS, read-only system partition, but broader Linux surface than Talos
Platform supportBroad: bare metal, VMs, cloud, edgeStrongest on AWS; also VMware and bare metal variantsBroad cloud and virtualization support
ExtensionsSystem extensions, Image Factory, extension services, release-bound GPU supportVariants and flavors such as NVIDIA and FIPSsystemd-sysext, containers, systemd units, sysext-bakery
Governance/companyOpen-source Talos by Sidero Labs; Sidero is a CNCF Silver MemberOpen-source project built by AWS; AWS supports AWS-provided buildsCNCF Incubating project since August 2024
Learning curveHighest if the team expects SSHModerate for AWS teams; lower inside EKS practicesLowest for Linux operations teams

Day-2 operations: upgrades, debugging, and extensions

The day-2 question matters more than installation. You will install the OS once per node lifecycle, but upgrade, debug, replace, patch, and explain incidents for years.

Talos is the most coherent if you want every node operation to be declarative and auditable. Upgrades are API calls against an installer image. Kubernetes and OS upgrades are explicit operations, not package side effects. Diagnostics come from talosctl: service status, logs, disks, network state, component health, and resource APIs. Runbooks need Talos resources, not shell commands.

Extensions in Talos are powerful but constrained. System extensions are included in generated boot media or installer images, commonly via Image Factory. Extension services can run early privileged services where static pods or DaemonSets are not enough. Hardware-specific work, such as NVIDIA support, is possible but release-bound.

Bottlerocket day-2 operations fit AWS lifecycle thinking. In EKS, Brupop handles in-place updates for existing Bottlerocket nodes. It drains nodes, uses the eviction API, respects PDBs, and limits rollout blast radius. New nodes still depend on the AMI or image you launch, so keep launch templates current.

Debugging Bottlerocket is less disruptive than Talos for teams that want controlled host access, because the OS supports admin and host containers. Treat that as break-glass access, not routine configuration.

Flatcar day-2 operations are the most familiar. Automatic updates arrive through channels. Production clusters usually use Stable; a subset can run Beta to catch issues earlier. Reboot behavior is controlled through documented strategies, and organizations can run their own Nebraska-style update infrastructure. Flatcar also has an LTS channel.

For extensions, Flatcar has become more modular through systemd-sysext: overlays on read-only /usr, prebuilt sysext-bakery images, Kubernetes binaries, CRI-O, K3s, Wasmtime, and cloud vendor tooling.

When to choose Talos

Choose Talos when Kubernetes is the product boundary of the node. If nothing should run on the host except Kubernetes machinery, Talos gives you the cleanest model. It is compelling for bare metal, edge clusters, private cloud, Proxmox/vSphere, and multi-cloud fleets where you want one operational abstraction.

Talos is also the strongest choice when security teams want a hard answer to SSH access, mutable root filesystems, package drift, and undocumented host changes. Pair it with Kubernetes security best practices and the node layer becomes easier to audit.

Do not choose Talos casually if your team depends on node-level manual debugging, package-installed host agents, or mixed workloads outside Kubernetes. Talos rewards teams that invest in runbooks, observability, automated replacement, and reproducible node configuration.

When to choose Bottlerocket

Choose Bottlerocket when most of your Kubernetes fleet runs on AWS, especially EKS, and your organization values AWS-native support paths. Bottlerocket aligns with EC2 images, EKS worker patterns, AWS-provided builds, and AWS support plans.

Bottlerocket also fits teams that want immutable nodes but cannot go all the way to Talos’s no-shell model. Admin and host containers provide controlled access, variants reduce ambiguity, and Brupop coordinates rollout.

The caveat is portability. Bottlerocket can run in non-AWS contexts, but the further you move from AWS, the more you should compare it against Talos and Flatcar on ecosystem, tooling, and support.

When to choose Flatcar

Choose Flatcar when you want a lower-risk transition from traditional Linux nodes to immutable infrastructure. It is strong for teams that already understand systemd, Ignition, CoreOS-style provisioning, SSH, update channels, and cloud images. It gives you a read-only base OS, automatic atomic updates, broad platform availability, and CNCF governance without rewriting every host-level assumption.

Flatcar is also useful when Kubernetes is not the only container workload, or when Kubernetes, standalone container hosts, and custom systemd-managed components coexist. It is less opinionated than Talos and less AWS-centered than Bottlerocket.

The risk is familiarity. If engineers SSH into nodes, add one-off files, and treat systemd units as snowflake configuration, the immutable base loses value. Manage Flatcar with boot-time config, versioned artifacts, controlled update channels, and replaceable nodes.

FAQ

Is Talos more secure than Bottlerocket or Flatcar?

It depends on what you mean by secure. Talos has the smallest operational attack surface because it removes SSH, shell access, and package management entirely. Bottlerocket and Flatcar also reduce attack surface compared with general-purpose distributions, but preserve different forms of host access and customization.

Can Bottlerocket run outside AWS?

Yes, but AWS remains its center of gravity. Official materials describe variants for Kubernetes worker nodes in EC2, VMware, and bare metal, and ECS variants for EC2. For non-AWS-first environments, compare platform support, image lifecycle, and support expectations against Talos and Flatcar.

Is Flatcar just old CoreOS?

No. Flatcar started as a fork of CoreOS Container Linux, but it is now an active CNCF Incubating project with its own release streams, platform support, update mechanisms, and systemd-sysext-based modularity.

Which one is easiest to migrate to from Ubuntu worker nodes?

Flatcar is usually easiest because it preserves systemd, SSH, and a recognizable Linux host model while adding immutable OS updates. Bottlerocket can be straightforward inside EKS. Talos is often the biggest shift because it changes how administrators access and debug nodes.

Final recommendation

For AWS-heavy EKS fleets, start with Bottlerocket unless portability or a no-shell requirement points elsewhere. For bare metal, edge, private cloud, and multi-cloud Kubernetes, put Talos at the top of the shortlist and validate debugging and extension workflows early. For teams modernizing from Ubuntu or legacy CoreOS-style infrastructure, Flatcar is the pragmatic bridge.

The bigger decision is whether your platform treats nodes as replaceable, versioned infrastructure. Once that is true, all three can work well. The best choice is the one whose failure modes match your team’s reality.

Sources

  • https://www.siderolabs.com/talos-linux
  • https://docs.siderolabs.com/talos/v1.13/overview/what-is-talos
  • https://docs.siderolabs.com/talos/v1.13/configure-your-talos-cluster/lifecycle-management/upgrading-talos
  • https://docs.siderolabs.com/talos/v1.13/build-and-extend-talos/custom-images-and-development/system-extensions
  • https://docs.siderolabs.com/talos/v1.13/build-and-extend-talos/custom-images-and-development/extension-services
  • https://docs.siderolabs.com/talos/v1.13/learn-more/image-factory
  • https://github.com/siderolabs/talos/releases
  • https://aws.amazon.com/bottlerocket/
  • https://aws.amazon.com/bottlerocket/faqs/
  • https://bottlerocket.dev/en/os/1.62.x/concepts/variants/
  • https://github.com/bottlerocket-os/bottlerocket
  • https://github.com/bottlerocket-os/bottlerocket/releases
  • https://github.com/bottlerocket-os/bottlerocket-update-operator
  • https://www.cncf.io/projects/flatcar-container-linux/
  • https://www.flatcar.org/
  • https://www.flatcar.org/releases
  • https://www.flatcar.org/docs/latest/provisioning/cl-config/from-cloud-config/
  • https://www.flatcar.org/docs/latest/setup/releases/update-strategies/
  • https://www.flatcar.org/blog/2024/04/os-innovation-with-systemd-sysext/