Separating workloads across several NodePools with weight and taints for general, GPU, spot-only, up to zone-specific pools; and best practices for running Karpenter across multiple clusters with consistent, auditable per-cluster policies.

In episode 9 you optimized costs with Spot priority, instance filters, and automatic interruption handling. Episode 10 takes you to a larger scale: how to organize many NodePools within one cluster, and how to treat Karpenter when the clusters being managed number more than one.
After this episode, you'll understand how to separate general, GPU, spot-only, and zone-specific workloads using weight and taints, plus per-cluster policy best practices for multi-cluster deployments.
A single NodePool serving all workloads is simple, but quickly becomes rigid. Database workloads need on-demand and large volumes, while batch workers prefer cheap spot. ML workloads need GPU instances, and latency-sensitive services need specific zones. Combining all of them into one NodePool forces every group to compromise.
A common separation:
When a pod can be scheduled by more than one NodePool, Karpenter decides which NodePool to use. The spec.weight field sets the preference: a NodePool with a larger weight is prioritized, and its default value is zero. For example, a general NodePool with weight 10 would be chosen before a spot-only NodePool with weight 5.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: general
spec:
weight: 10
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
disruption:
consolidationPolicy: WhenUnderutilized
consolidateAfter: 1mTip
Weight is only used to pick a NodePool when a pod actually qualifies for all candidates. A pod with a strict nodeSelector or toleration will still fall to the matching NodePool, regardless of weight.
Weight manages preference, but taints manage permission. A GPU NodePool is tainted so ordinary pods can't get in, and only pods with the nvidia.com/gpu toleration are scheduled there.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: gpu-pool
spec:
weight: 20
template:
spec:
taints:
- key: nvidia.com/gpu
effect: NoSchedule
requirements:
- key: node.kubernetes.io/instance-type
operator: In
values: ["g5.xlarge", "g5.2xlarge"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: defaultWith this configuration, ordinary web workloads will never run on expensive GPU nodes.
For workloads that need a particular location — for instance due to proximity to a database or data regulations — a NodePool can be limited to a specific zone via the topology.kubernetes.io/zone label.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: az-a
spec:
template:
spec:
requirements:
- key: topology.kubernetes.io/zone
operator: In
values: ["us-east-1a"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: defaultImportant
Make sure the subnet for that zone really exists on the EC2NodeClass being used. A zone requirement unsupported by subnets keeps pods Pending forever.
| NodePool | Capacity | Taint | Weight | Target workload |
|---|---|---|---|---|
| general | Spot + On-Demand | No taint | 10 | Ordinary web apps |
| spot-only | Spot only | No taint | 5 | Tolerant batch jobs |
| gpu-pool | GPU on-demand | nvidia.com/gpu | 20 | Training and inference |
| az-a | All types | No taint | 0 | Zone-specific services |
Karpenter is a controller that lives inside one cluster and manages that cluster's nodes. There's no central Karpenter managing many clusters. When you have ten clusters, that means ten separately running Karpenter instances — each with its own NodePools, NodeClasses, and disruption policies.
The benefit of this isolation is a small blast radius. A configuration error in one cluster doesn't spread to another. But the consequence is that every cluster needs to be managed with the same discipline to avoid configuration drift between environments.
The first multi-cluster best practice is consistency through code. Define NodePools and NodeClasses as files managed via GitOps, for example with ArgoCD or Flux, then apply them to all clusters from a single source. This way, differences between clusters can be reduced to only the things that genuinely must differ.
kubectl apply -f clusters/staging/karpenter/
kubectl apply -f clusters/production/karpenter/Additionally, watch out for the following:
Staging clusters can usually run aggressively on cost savings, while production needs more safety nets. The following configuration illustrates that difference.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: general
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 15mWarning
Running many clusters also means managing many SQS queues and IAM policies for each one. Don't let a queue be shared by two clusters — scope resources per cluster so they don't swallow each other's events.
A large weight doesn't prevent misplaced workloads if taints aren't set. Use both together: weight for preference, taints for hard restrictions.
Forcing staging and production to use identical NodePools ignores the differences in cost and stability needs. Adjust policies per environment, but keep a single source of truth in Git.
Multi-NodePool and multi-cluster are two levels of organization that must be designed, not left to grow wild. Weight, taints, and zone isolation keep one cluster organized; GitOps and per-environment policies keep many clusters in sync.
Key takeaways:
In episode 11, you'll learn about drift detection — how Karpenter detects differences between the NodePool and NodeClass templates and actual nodes, plus the NodeOverlay feature for updating nodes without a full replacement. See you there!