Last week I ran into a familiar mess, pods landing on nodes before the CNI plugin was actually ready. Kubelet marks the node as Ready, scheduler starts placing workloads, then everything sits in ContainerCreating because Calico is still coming up. I have worked around this with init containers and postStart tricks for way too long.

I came across the Node Readiness Controller announcement on the Kubernetes blog. It is a new SIG project (v0.1.1), and it is basically what I wanted, custom readiness gates for nodes managed through a CRD.

The Problem

Kubernetes gives us one main readiness signal for nodes, the Ready condition. The problem is that kubelet Ready does not always mean ready for real workloads. In practice I still need things like:

  • CNI plugin fully initialized
  • GPU drivers loaded and verified
  • Storage CSI drivers registered
  • Custom health checks passing
  • Heavy container images pre-pulled

I have handled this with taints plus scripts, DaemonSet probes, and even a rough CronJob that removed taints after checking node conditions. It worked, but it was brittle.

Enter NodeReadinessRule

The controller adds a NodeReadinessRule CRD. I declare the conditions a node must satisfy, and the controller handles taints for me. This is an example I used for CNI readiness:

apiVersion: readiness.node.x-k8s.io/v1alpha1
kind: NodeReadinessRule
metadata:
  name: cni-readiness
spec:
  conditions:
    - type: "cniplugin.example.net/NetworkReady"
      requiredStatus: "True"
  taint:
    key: "readiness.k8s.io/network-unavailable"
    effect: "NoSchedule"
    value: "pending"
  enforcementMode: "bootstrap-only"
  nodeSelector:
    matchLabels:
      node-role.kubernetes.io/worker: ""

What this does is:

  1. Applies a NoSchedule taint to matching worker nodes
  2. Watches for the NetworkReady condition to become True
  3. Removes the taint once it’s satisfied
  4. Stops monitoring after the first successful check because it is bootstrap-only

Two Enforcement Modes

There are two enforcement modes:

bootstrap-only. Checks once during node startup, then stops. Good for one-time setup like image pre-pulling or driver install.

continuous. Keeps watching the condition for the full node lifetime. If a GPU driver dies at 3 AM, the node is tainted again right away, and new pods stop landing there.

For GPU nodes, I would absolutely use continuous mode:

apiVersion: readiness.node.x-k8s.io/v1alpha1
kind: NodeReadinessRule
metadata:
  name: gpu-driver-readiness
spec:
  conditions:
    - type: "gpu.mycompany.io/DriverHealthy"
      requiredStatus: "True"
  taint:
    key: "readiness.k8s.io/gpu-unavailable"
    effect: "NoSchedule"
    value: "unhealthy"
  enforcementMode: "continuous"
  nodeSelector:
    matchLabels:
      accelerator: "nvidia-a100"

Setting Up Condition Reporting

The controller does not run health checks itself, it only reacts to node conditions. So I still need something to report those conditions. I see two practical options:

1. Node Problem Detector (NPD). If I already run it, I can add scripts that patch node conditions.

2. Readiness Condition Reporter. A small agent from this project. I deploy it as a DaemonSet, point it to a local HTTP endpoint, and it patches conditions:

# Install the controller
kubectl apply -f https://github.com/kubernetes-sigs/node-readiness-controller/releases/download/v0.1.1/install.yaml

# Check it's running
kubectl get pods -n node-readiness-system

Dry Run Mode for Safety

One thing I like a lot is dry run mode. Before I enforce rules cluster-wide, I can see impact first:

spec:
  enforcementMode: "continuous"
  dryRun: true

In dry run, the controller logs what it would do and updates rule status with affected nodes, but it does not apply taints. I tested this in staging and caught a bad selector that would have tainted half our worker pool.

My Take

This is still alpha (v0.1.1), so I am not putting it on mission-critical production clusters yet. Still, the design looks solid:

  • Declarative. No more hand-written taint scripts.
  • Decoupled. Works with any condition reporter, not only the bundled one.
  • Safe. Dry run, bootstrap-only behavior, and clear status reporting.

The integration with existing Node Problem Detector setups matters a lot for me. Most of my clusters already run NPD, so adding readiness rules is straightforward.

If you run mixed clusters with GPU nodes, high-memory pools, or special hardware, this fixes a very real scheduling headache. I am planning to roll it out on our dev clusters next week and then write a follow-up with real results.

If you want to try it, start with the docs and the GitHub repo. There is also a maintainer track session at KubeCon EU 2026.