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 u2014 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 u2014 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 u2014 the article’s framework u2014 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

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 u2014 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 u2014 storage, agents u2014 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 u2014 and it is the discovery half of most custom-readiness setups.

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

No u2014 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

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.

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/

OpenSearch, Loki, Quickwit and ClickHouse: Which Platform Should You Choose for Logs and Observability in 2026?

OpenSearch, Loki, Quickwit and ClickHouse: Which Platform Should You Choose for Logs and Observability in 2026?

Log management and observability are no longer a matter of “storing text and searching it quickly.” In 2026, the real challenge is economic and architectural: ingesting growing volumes, retaining data for months, correlating logs with metrics and traces, and responding during an incident without costs spiraling out of control.

For years, Elasticsearch was the dominant option. Then came OpenSearch, Loki, Quickwit and, with significant momentum, ClickHouse as an analytical backend. All of them can solve part of the problem, but they do not optimize for the same things. The senior decision should not be “which one is faster” in the abstract, but which questions the team will ask, what retention it needs and how much complexity it can operate.

The Historical Standard: Elasticsearch and OpenSearch

OpenSearch began as an open source fork of Elasticsearch and keeps the mental model most familiar to many teams: indices, shards, replicas, mappings, Query DSL, aggregations and a mature full-text experience. If your organization already comes from Elasticsearch, it is the most direct transition.

Architecturally, it is built on Apache Lucene. Each index is divided into shards; each shard maintains segments and inverted indices; the cluster coordinates writes, replicas, distributed searches and aggregations. This provides a great deal of power, but also a clear operational footprint: JVM, heap sizing, merges, hot/warm/cold tiers and mapping tuning.

For logs, OpenSearch shines when you need expressive text search, rich filters, aggregations, mature dashboards, existing tooling or use cases close to SIEM. The cost appears as volume grows: indexing a lot of content makes it fast to search, but consumes storage and memory.

In 2026, the 3.x branch also matters. OpenSearch 3.0 introduced relevant changes such as Lucene 10 and JVM 21, along with performance improvements and vector search capabilities. That signals project vitality, but it also means upgrades need more careful planning than in a purely stateless stack.

Loki: Observability Centered on Efficiency

Grafana Loki takes a different philosophy: do not index the full content of logs, only the labels. In practice, it is closer to “Prometheus for logs” than to Elasticsearch: queries by labels and subsequent filtering over lines with LogQL.

Its storage is based on compressed chunks and a small index. Chunks usually live in object storage such as S3, GCS, Azure Blob or MinIO, while the index points to streams defined by label combinations. That decision reduces cost, but shifts responsibility to label design.

The practical rule is simple: labels must have controlled cardinality. cluster, namespace, app, environment or service usually work well. request_id, user_id or trace_id as labels can break the model because they multiply streams, hurt compaction and grow the index. For those values, it is better to filter log content or rely on traces.

Loki shines in Kubernetes because the label model fits pods, namespaces and services. Its Grafana integration is also excellent: log exploration, jumps from metrics, alerts with LogQL and correlation with Tempo or Prometheus.

Its weakness appears when you want to treat logs as an arbitrary search corpus. A query like “find this fragment across all logs from the last six months” can be much more expensive than in a system with a full-text index. Loki is designed for queries bounded by labels and time, not to replace a general-purpose search engine.

In 2026, Loki remains highly active. The official documentation lists recent 3.x versions, including the 3.7 branch, and Grafana continues to position it as a horizontally scalable, highly available and cost-efficient log aggregation system. Grafana’s acquisition of Logline further reinforces the focus on difficult searches within logs, although it does not change the central principle: label design remains critical.

When Loki Shines

Loki is the pragmatic choice for Kubernetes teams that already live in Grafana, need to retain volume at low cost and query by service, namespace, cluster, environment and time window. It is less convincing for global full-text search or deep forensic analysis.

Quickwit: Cloud-Native Search on Object Storage

Quickwit is a distributed search platform written in Rust, designed for append-only workloads such as logs and traces. Its thesis is attractive: keep powerful search, but decouple compute and storage by using object storage as the main layer.

Instead of depending on hot local disks, Quickwit writes indices and splits to S3-compatible storage, Azure Blob, Google Cloud Storage or MinIO. Indexers process data, searchers query segments and the metastore coordinates state. The model uses inverted indices optimized for immutable data and remote storage: closer to OpenSearch in search than Loki, but with cloud-native economics.

Quickwit is especially interesting for teams that want to replace part of an expensive Elasticsearch/OpenSearch cluster, already have logs in S3 or can move them there, and need text search with a better operational cost profile. It also has Grafana integration and APIs that are partially compatible with Elasticsearch/OpenSearch, which helps with some existing workflows.

The critical nuance for 2026 is its project status. Datadog acquired Quickwit in 2024. The acquisition did not mean the open source repository disappeared: the quickwit-oss/quickwit repository remains public and shows recent activity, including 2026 tags and discussions. The Quickwit Grafana plugin also publishes recent compatibility with Grafana 12.1+ and 13. That said, the product risk is different from Loki or OpenSearch: strategic direction is influenced by Datadog, and part of the effort may be oriented toward internal or commercial use cases.

The honest reading is this: Quickwit should not be dismissed as “dead,” because according to the reviewed sources it is not archived. But it should be evaluated with more diligence: release cadence, critical issues, roadmap, real API compatibility, operational ease and dependency on the maintainer team. For a core observability platform, that evaluation matters as much as the benchmark.

ClickHouse: The Analytical Engine of New Observability

ClickHouse was born as a columnar analytical database, not as a log platform. That is exactly why it has become relevant to observability. Many incidents are not solved by searching for an exact string, but by grouping by service, calculating percentiles, exploring cardinalities, joining logs with traces and querying billions of events over a time range.

ClickHouse’s model is columnar. Data is stored by columns, compresses extremely well and is queried with vectorized execution, ordered primary indices, data skipping indexes and engines such as MergeTree. For observability, this makes it possible to store wide events and read only the columns needed.

The tradeoff is that ClickHouse is not Elasticsearch. Full-text search exists and has improved, but its strength remains structured and semi-structured analysis. If your queries are “give me all 500 errors by version, region and endpoint in the last 15 minutes,” ClickHouse is a very good fit. If your main query is “find any line containing a rare phrase across six months of unstructured text,” you probably want another component or a hybrid strategy.

ClickHouse has moved decisively toward observability. The acquisition of HyperDX and the launch of ClickStack change its positioning: it is no longer only “use ClickHouse as the backend and build the UI yourself,” but a stack with OpenTelemetry, HyperDX as the interface and ClickHouse as the engine. It shines in high-volume platforms, structured logs, traces, derived metrics and low-latency aggregate queries, although it requires understanding modeling, TTLs, compression and ingestion costs.

Quick Comparison

CharacteristicOpenSearchLokiQuickwitClickHouse
Main modelDistributed search on LuceneLogs by streams and labelsCloud-native distributed searchColumnar analytical database
Storage modelShards, Lucene segments, hot/warm/cold tiersCompressed chunks + small indexIndices/splits in object storageCompressed columns in MergeTree
IndexInverted, very completeLabels, not full contentInverted, optimized for object storagePrimary index, data skipping, columns
Full-text searchExcellentLimited and dependent on filtersVery goodGood, not its main strength
Relative costHighLowLow-mediumLow-medium
Query latencyLow on well-sized hot indicesLow if bounded by labels; worse on broad scansLow-medium depending on cache and object storageVery low on well-modeled aggregations
ScalingMature, but operationally involvedHorizontal and economicalDecouples compute and storageExcellent for analytics and ingestion
KubernetesGoodExcellentGoodGood, better with ClickStack/OTel
GrafanaYesNativeYesYes
Elasticsearch compatibilityHigh in OpenSearchNoPartialNo
Best use caseComplex search and compatibilityLow-cost Kubernetes logsEfficient search on S3Unified analytical observability
Main riskCost and complexityLabel cardinalityEcosystem and governance after acquisitionModeling and SQL/operations learning curve

Which Should You Choose in 2026?

The answer depends less on technology fashion and more on the team’s profile.

If You Are a Platform Team with Elasticsearch Heritage

OpenSearch is the conservative option if you already have automation, alerts and operational knowledge around Elasticsearch/OpenSearch. Do not migrate “as is” expecting costs to drop: review mappings, retention, ILM/ISM, shards, indexed fields and the separation between hot data and archive.

If You Are a Kubernetes Team Centered on Grafana

Loki is usually the first option. It is efficient, integrates very well with Grafana and reduces the cost of operational logs. The key is to govern labels from day one: allowed taxonomy, cardinality limits and context-driven queries before text searches.

If You Want to Reduce Cost Versus OpenSearch Without Losing Search

Quickwit deserves a serious PoC when Loki falls short on search and OpenSearch is expensive. The decision must include continuity: repository activity, releases, clients, community support, Grafana integration and Datadog’s position.

If You Want a Unified Observability Platform

ClickHouse is probably the strongest bet for consolidating logs, traces, derived metrics and events in a shared analytical database. ClickStack and HyperDX reduce the need to build the entire experience from scratch, but adoption depends on maturity in SQL, modeling and analytical database operations.

If You Have Security, Audit or SIEM Requirements

OpenSearch remains more natural for textual investigation and security tooling. ClickHouse can work for security analytics at scale, but it requires modeling. Loki is rarely the first choice for SIEM. Quickwit can be interesting, with a smaller ecosystem.

If Budget Is the Bottleneck

Do not choose only by storage cost per TB. Calculate total cost: ingestion, queries, retention, operations, backups, upgrades, training and incident time. Loki and ClickHouse usually win on raw cost; Quickwit can be highly competitive on object storage; OpenSearch can be reasonable if it is limited to data that is truly searchable and hot.

How to Frame a Decision PoC

A useful test is not installing four Helm charts and looking at a demo. Define real questions from past incidents and run them against representative data:

  • The last 7 days of production logs, with real cardinality.
  • A hot 2-hour window with high volume.
  • A rare text search over a broad period.
  • An aggregation by service, endpoint, version and region.
  • A correlation flow from alert to logs and traces.

Measure sustained ingestion, storage cost, p50/p95 latency, operational complexity and team experience. Include an uncomfortable scenario: a failed node, slow object storage, unexpected cardinality or a badly written query.

Conclusion

The era when Elasticsearch was the only viable option for logs is over. In 2026, there are four clear paths.

OpenSearch is the mature choice for full-text search and compatibility. Loki is efficient for operational logs in Kubernetes. Quickwit offers modern search on object storage, with mandatory governance evaluation after the Datadog acquisition. ClickHouse is consolidating as an analytical engine for unified observability, especially with ClickStack and HyperDX.

The best platform is not the one that wins every benchmark. It is the one that answers your production questions with the lowest sustainable cost and the least operational friction for your team.

CTA: Decide with Data, Not Preferences

Before migrating or standardizing, run a two-week PoC with real data, real queries and an honest estimate of total cost. It should reveal which data belongs in search, which belongs in analytics and which belongs in cheap storage.

Sources

  • OpenSearch 3.0: https://opensearch.org/blog/unveiling-opensearch-3-0/
  • OpenSearch 3.0, Lucene 10 and JVM 21: https://opensearch.org/blog/opensearch-3-0-what-to-expect/
  • Grafana Loki, documentation: https://grafana.com/docs/loki/latest/
  • Grafana Loki, releases 3.x: https://grafana.com/docs/loki/latest/release-notes/
  • Grafana Loki, labels and cardinality: https://grafana.com/blog/how-labels-in-loki-can-make-log-queries-faster-and-easier/
  • Datadog acquires Quickwit: https://www.datadoghq.com/blog/datadog-acquires-quickwit/
  • Quickwit OSS: https://github.com/quickwit-oss/quickwit
  • Quickwit datasource for Grafana: https://github.com/quickwit-oss/quickwit-datasource
  • ClickStack: https://clickhouse.com/clickstack
  • ClickStack/HyperDX in ClickHouse Cloud: https://clickhouse.com/docs/use-cases/observability/clickstack/overview
  • ClickHouse acquires HyperDX: https://clickhouse.com/blog/202504-newsletter
  • ClickHouse and observability 2026: https://clickhouse.com/resources/engineering/what-is-observability

Kamal vs Kubernetes: An Honest Comparison for Teams Who Don’t Need 1,000 Services

Kamal vs Kubernetes: An Honest Comparison for Teams Who Don't Need 1,000 Services

The framing problem

Most “Kamal vs Kubernetes” articles are written by people who just discovered Kamal and are excited about it. They frame it as a simpler alternative that embarrasses Kubernetes. That framing is wrong and it will lead you to the wrong decision.

Kamal is not better than Kubernetes. Kubernetes is not overkill for everyone. They solve different problems at different scales, and picking the wrong one for your context is expensive either way.

This article gives you the honest version: what each tool actually is, where each one breaks, and the specific signals that should push you in one direction or the other.


What Kamal actually is

Kamal (formerly MRSK) is a deployment tool built by 37signals. It deploys Docker containers to servers via SSH, uses kamal-proxy as its default reverse proxy, and handles rolling deploys with zero downtime. That is the entire feature set.

kamal deploy

It connects to your servers over SSH, pulls the new image, starts the new container, waits for it to be healthy, then moves traffic through kamal-proxy and stops the old one. Traefik is still possible, but in Kamal 2 it sits in front of kamal-proxy as an optional accessory rather than being the standard path.

There is no control plane. No etcd. No API server. No scheduler. No concept of desired state reconciliation. Docker or systemd can restart a crashed container if you configure the right restart policy, but Kamal itself does not reschedule workloads to another node or provide cluster-level self-healing.

Kamal is a deployment tool. Kubernetes is an orchestration platform. These are not the same category.

That is the key distinction from tools like Nomad. I covered that angle in Nomad vs Kubernetes, as part of the same “lighter alternatives to Kubernetes” discussion. Nomad is a scheduler/orchestrator. Kamal is a deployment tool that leaves scheduling and node-level recovery mostly outside its scope.


What Kubernetes actually is

Kubernetes is a distributed system that continuously reconciles actual state against desired state. You declare what you want — N replicas of container X, with these resource limits, this health check, this rollout strategy — and the control plane makes it happen and keeps it that way.

When a node fails, your pods are rescheduled. When a container crashes, it is restarted. When you push a new image, the rollout respects your maxUnavailable and maxSurge settings. When traffic spikes, HPA can add replicas.

This machinery is powerful. It is also genuinely complex to operate.

A minimal production-grade Kubernetes cluster involves: a multi-node control plane for HA, a CNI plugin, an ingress controller (or Gateway API), cert-manager for TLS, a CSI driver for storage, a metrics server for HPA, RBAC policies, network policies, pod disruption budgets, and some form of secret management. Getting all of this right takes time and ongoing maintenance.

The question is whether the problems Kubernetes solves are problems you actually have.


The minimum config tells the story

Here is the same tiny web service expressed in both worlds. The exact fields vary by app, registry, ingress setup, and Kubernetes distribution, but the shape is representative.

<table> <thead> <tr> <th>Kamal <code>deploy.yml</code></th> <th>Kubernetes <code>Deployment</code> + <code>Service</code></th> </tr> </thead> <tbody> <tr> <td> <pre><code class=”language-yaml”>service: web image: ghcr.io/acme/web

servers: web: hosts: – 203.0.113.10

proxy: ssl: true host: app.example.com app_port: 3000

registry: username: acme password: – KAMAL_REGISTRY_PASSWORD

env: secret: – DATABASE_URL</code></pre> </td> <td> <pre><code class=”language-yaml”>apiVersion: apps/v1 kind: Deployment metadata: name: web spec: replicas: 2 selector: matchLabels: app: web template: metadata: labels: app: web spec: containers: – name: web image: ghcr.io/acme/web ports: – containerPort: 3000 envFrom: – secretRef: name: web-secrets — apiVersion: v1 kind: Service metadata: name: web spec: selector: app: web ports: – port: 80 targetPort: 3000</code></pre> </td> </tr> </tbody> </table>

The Kubernetes example is still incomplete for internet traffic and TLS: you would normally add an Ingress or HTTPRoute, cert-manager, DNS, and secret provisioning. Kamal’s example is closer to “deploy this app to these servers.” Kubernetes is closer to “declare this workload inside a platform that already exists.”


The 37signals case

37signals (the company behind Basecamp and Hey) moved off cloud Kubernetes to bare-metal servers running Kamal in 2022-2023. Their own Ops team wrote about the “de-cloud and de-k8s” move. The story got a lot of attention.

What the hot takes missed: 37signals is not a startup. They have a dedicated ops team. They own and operate hardware. Their workload is well-understood and stable: a small number of mature applications serving a large, established user base.

Their decision was rational for their context. Kubernetes was costing them in cloud spend and operational complexity for problems their workload does not have. Moving to Kamal on owned hardware made economic and operational sense for them.

It does not follow that Kamal is the right choice for your early-stage startup, your company that runs on AWS because your team does not want to manage hardware, or your platform that needs to scale from 0 to 10x on short notice.


Where Kamal breaks

No cluster-level self-healing. If your container crashes, Docker or systemd can restart it with the right policy, but Kamal has no involvement. If your server fails, Kamal will not reschedule that workload elsewhere; your service is down until you intervene or until you have set up something else to handle it.

No built-in auto-scaling. Kamal has no equivalent of HPA or KEDA. If your traffic spikes, you add servers manually or script something yourself.

No multi-AZ HA by default. You can deploy to multiple servers across regions, but the coordination is yours to manage. There is no concept of spreading replicas across availability zones automatically.

Rolling deploys are basic. Kamal’s zero-downtime deploy is good for single-server or simple multi-server setups. It is not a substitute for Kubernetes rolling update with fine-grained health check integration across a fleet.

Secret management is manual. Kamal reads secrets from .kamal/secrets and has kamal secrets helpers for fetching and extracting values from password managers and cloud secret stores, but there is no Kubernetes-style projected secret lifecycle. Rotation and policy remain yours to manage.

No workload isolation. Everything on the same server shares the same kernel, same resources. Resource limits are Docker-level, not enforced by an orchestration layer with quotas and namespaces.


Where Kubernetes breaks

The complexity tax is real. Running Kubernetes yourself — not a managed service, but actual cluster operation — requires dedicated expertise. A team without a platform engineer will spend significant time debugging networking issues, understanding why pods are Pending, or figuring out why cert-manager is not issuing certificates.

Managed K8s is not free. EKS charges for the cluster control plane, with standard support priced around $0.10/hour per cluster; GKE and AKS have their own pricing rules and credits. In all cases, you still pay for nodes, load balancers, and persistent volumes. For a small application, this cost can exceed what bare-metal hosting would cost.

The YAML surface area scales with complexity. A simple web app on Kubernetes requires a Deployment, a Service, an Ingress (or HTTPRoute), a Certificate, potentially a HorizontalPodAutoscaler, a PodDisruptionBudget, and NetworkPolicies. That is a lot of infrastructure for a CRUD app.

Debugging is harder. Distributed systems are harder to debug than single-server setups. kubectl exec and kubectl logs are good tools but diagnosing why traffic is not reaching your pod — DNS, CNI, network policy, service selector, readiness probe — requires Kubernetes-specific knowledge that takes time to build.


The honest decision framework

Ask yourself these questions in order:

1. Do you have more than one team working on independent services with different scaling needs?

If yes: Kubernetes starts making sense. The isolation and per-service resource management are worth the overhead.

If no: Kamal is probably sufficient.

2. Do you need to survive individual server failures automatically, without manual intervention?

If yes: You need either Kubernetes with proper HA setup, or a managed container platform whose recovery and placement model fits your app. Cloud Run, Fly.io, Render, and Railway all help here, but their HA boundaries differ. Kamal alone does not give you this.

If no: A Docker or systemd restart policy covers the common crash case.

3. Does your traffic profile require automatic scaling?

If yes: Kubernetes with HPA or KEDA, or a managed platform with the scaling behavior you actually need. Cloud Run scales request-driven services to and from zero; Fly.io supports autostart/autostop and machine scaling; Render and Railway support scaling, but with provider-specific limits and billing. Kamal has no answer here without custom tooling.

If no: Fixed capacity is fine. Kamal works.

4. Do you have a platform team, or is “ops” a shared responsibility among product engineers?

If platform team exists: Kubernetes operational overhead is distributed and manageable.

If ops is everyone’s side job: Kamal’s simplicity is a genuine advantage. Less to go wrong, less to learn, faster to debug.

5. Are you on cloud infrastructure you do not own, and cloud costs matter?

If yes: Run the numbers on managed K8s vs a few VPS instances. For small workloads, VPS + Kamal is significantly cheaper.

If no: You have hardware already. Kamal is almost certainly the right choice.


Side-by-side

KamalManaged container platformsKubernetes
ExamplesKamal on VPS/bare metalCloud Run, Fly.io, Render, RailwayEKS, GKE, AKS, self-managed
Deployment modelSSH + DockerPlatform API / Git deploy / container deployAPI-driven, reconciliation loop
Self-healingDocker/systemd restart policy onlyPlatform-managed restart and placementPod rescheduling, node failure recovery
Auto-scalingManualProvider-specific; not all scale the same wayHPA, VPA, KEDA
HAManual (multiple servers)Provider-managed, with platform limitsAvailable with a well-configured multi-node control plane (not free)
TLSkamal-proxy with automatic HTTPS; Traefik optionalUsually built incert-manager + ingress controller
Secret management.kamal/secrets + helper commandsPlatform secretsSecrets API, external-secrets, Vault
ObservabilityStandard Docker loggingProvider logs/metrics, export variesRich ecosystem (Prometheus, OTel, etc.)
Rollout controlBasic rolling deploySimple rollouts, provider-specificFine-grained (maxUnavailable, canary, etc.)
Learning curveLowLow to mediumHigh
Ops overheadLowLowMedium to high
Right scale1-20 services, stable loadSmall to medium teams that want managed recovery/scaling10+ services, variable load, or regulated

What the threshold actually looks like

Kamal is the right default if you are:

  • A team of 2–10 engineers shipping a web application
  • Running stable, predictable workloads
  • On a budget where managed K8s costs matter
  • Without a dedicated platform engineer

Kubernetes is the right choice when:

  • You have 10+ services with independent deployment cycles
  • You need multi-zone HA and automatic failover
  • Traffic is variable enough that auto-scaling saves real money or prevents real incidents
  • You have a platform team that can absorb the operational complexity
  • You are in a regulated environment that benefits from K8s’s audit trails and RBAC

There is a middle tier worth mentioning: managed container platforms like Fly.io, Railway, Render, and Google Cloud Run. These give you some of Kubernetes’s operational benefits with closer to Kamal’s operational simplicity, but the details matter: HA, auto-scaling, scale-to-zero, regions, and billing differ by provider. For teams that need more than Kamal but do not want to operate K8s, this tier is often the right answer and gets underrepresented in the debate.


The real lesson from 37signals

The story is not “Kamal beat Kubernetes.” The story is: match your infrastructure to your actual operational profile, not to what is fashionable or what scales to Google’s size.

37signals evaluated what problems they actually had and picked the tool that solved those problems at the lowest operational cost. That is the right framework.

For most teams reading this, Kubernetes is probably the right answer eventually — when your system’s complexity justifies it. The mistake is adopting it before you are there, burning engineering time on infrastructure problems instead of product problems.

Kamal is a good tool for a specific stage and scale. Use it until you outgrow it. When you outgrow it, you will know — because the things Kubernetes solves will be actual problems you have, not hypothetical ones.


What to do next

Before you choose, write down your actual requirements in one page: number of services, expected traffic variance, failure tolerance, who owns ops, whether you own hardware, and what compliance needs are real today. Then run a small proof of concept with the simplest tool that satisfies those requirements. If Kamal plus Docker restart policies covers the failure modes you actually accept, start there. If you need automatic placement, autoscaling, and multi-zone recovery now, compare managed container platforms first, then Kubernetes.


FAQ

Can Kamal and Kubernetes coexist?

Yes. Some teams use Kamal for simple auxiliary services (internal tools, cron jobs, simple APIs) while running their core platform on Kubernetes. The tools are not mutually exclusive.

Is Kamal production-ready?

Yes. 37signals runs Basecamp and Hey on it. It handles zero-downtime deploys, TLS, and multi-server deployments reliably. The limitation is not reliability — it is feature scope.

What about Docker Swarm? Is it still relevant?

Docker Swarm fills a similar niche — simpler than Kubernetes, multi-host orchestration. Swarm mode is still documented and supported as part of Docker Engine for teams that want it as a production runtime, but its ecosystem is much smaller than Kubernetes and less active in the market. I would not pick it as the default for a new project unless the team already knows and wants Swarm.

Does Kamal work with any cloud provider?

Yes. Kamal only requires SSH access to a server and a container registry. It works with any VPS provider (Hetzner, DigitalOcean, OVH, AWS EC2, etc.) and any registry (Docker Hub, GitHub Container Registry, ECR, Harbor).

Is Kubernetes worth learning even if you use Kamal today?

Yes. Kubernetes is the dominant orchestration platform. Understanding it — even without operating it — makes you a better platform engineer and opens more career options. Learning Kamal does not preclude learning Kubernetes.