Karpenter NodeOverlays in Practice: Three Scenarios You'll Actually Use
Three practical scenarios for the Karpenter v1alpha1 NodeOverlay feature, with sample YAML.
TL;DR
- Karpenter makes instance selection and replacement decisions based on price, but the only prices it references are the AWS Pricing API (On-Demand public price) and the real-time Spot price.
- Account-level discounts like Savings Plans or Reserved Instances, per-instance license costs, and custom resources exposed via Device Plugins are not included in this calculation.
- NodeOverlay fills this gap by overriding the price or Extended Resources during the scheduling simulation.
- This post walks through three scenarios with samples:
- Reflecting the effective unit cost (SP / RI, etc.)
- Making Extended Resources visible to the scheduler
- Applying a soft penalty to specific instance families
Heads up: NodeOverlay is still Alpha (
karpenter.sh/v1alpha1). The schema may change, so if you adopt it in production, make re-validation on every upgrade part of your routine.
Prerequisites
1. Verify the CRD
You need a Karpenter version that ships with NodeOverlay, and the CRD must be present.
kubectl get crd nodeoverlays.karpenter.sh kubectl api-resources | grep -i nodeoverlay
2. Enable the Feature Gate (required)
Because NodeOverlay is an Alpha feature, it is disabled in the default Karpenter configuration. You need to flip settings.featureGates.nodeOverlay to true in the Helm chart to enable it.
Check current state
# Inspect only the featureGates section of the current values helm get values karpenter -n kube-system --all | yq '.settings.featureGates' # You can also check the FEATURE_GATES env var on the Karpenter Pod kubectl -n kube-system get pod -l app.kubernetes.io/name=karpenter \ -o jsonpath='{.items[0].spec.containers[0].env}' \ | jq '.[] | select(.name=="FEATURE_GATES")'
Enable via Helm
Using Helm CLI --set
# # - `--reuse-values`: keeps the existing values as-is. Without this flag, required settings # like `clusterName` and `interruptionQueue` may be reset to chart defaults. # - `--version`: pins to the currently installed version. If omitted, the chart auto-upgrades # to the latest version, which upgrades Karpenter itself along with it. # Check the currently installed chart version first (to pin the upgrade) CHART_VERSION=$(helm list -n kube-system -o json \ | jq -r '.[] | select(.name=="karpenter") | .chart' \ | sed 's/karpenter-//') helm upgrade karpenter oci://public.ecr.aws/karpenter/karpenter \ --version "${CHART_VERSION}" \ --namespace kube-system \ --reuse-values \ --set settings.featureGates.nodeOverlay=true
Using a karpenter-values.yaml file
... settings: clusterName: {CLUSTER_NAME} featureGates: nodeOverlay: true ...
# Pin the current chart version CHART_VERSION=$(helm list -n kube-system -o json \ | jq -r '.[] | select(.name=="karpenter") | .chart' \ | sed 's/karpenter-//') helm upgrade karpenter oci://public.ecr.aws/karpenter/karpenter \ --version "${CHART_VERSION}" \ --namespace kube-system \ -f karpenter-values.yaml
Verify the change
# Confirm that NodeOverlay=true is present in FEATURE_GATES on the restarted Pod kubectl -n kube-system get pod -l app.kubernetes.io/name=karpenter \ -o jsonpath='{.items[0].spec.containers[0].env}' \ | jq '.[] | select(.name=="FEATURE_GATES")'
Changing a feature gate triggers a restart of the Karpenter controller Pod. New provisioning and consolidation pause briefly during the restart, so it's best to avoid high-traffic windows.
3. Test EC2NodeClass / NodePool
Use the YAML below. Replace CLUSTER_NAME with your own cluster name.
01-ec2nodeclass.yaml
apiVersion: karpenter.k8s.aws/v1 kind: EC2NodeClass metadata: name: overlay-test spec: amiFamily: AL2023 role: "KarpenterNodeRole-${CLUSTER_NAME}" subnetSelectorTerms: - tags: karpenter.sh/discovery: "${CLUSTER_NAME}" securityGroupSelectorTerms: - tags: karpenter.sh/discovery: "${CLUSTER_NAME}" amiSelectorTerms: - alias: al2023@latest tags: Purpose: karpenter-nodeoverlay-test
02-nodepool.yaml
apiVersion: karpenter.sh/v1 kind: NodePool metadata: name: overlay-test spec: template: metadata: labels: purpose: nodeoverlay-test spec: nodeClassRef: group: karpenter.k8s.aws kind: EC2NodeClass name: overlay-test requirements: - key: kubernetes.io/arch operator: In values: ["amd64"] - key: kubernetes.io/os operator: In values: ["linux"] - key: karpenter.sh/capacity-type operator: In values: ["on-demand", "spot"] - key: node.kubernetes.io/instance-type operator: In values: ["m5.large", "m5.xlarge", "m5.2xlarge", "m6i.large", "m6i.xlarge"] expireAfter: 720h limits: cpu: "64" memory: 256Gi disruption: consolidationPolicy: WhenEmptyOrUnderutilized consolidateAfter: 30s
Scenario 1. Reflect the Effective Unit Cost (SP / RI) in Scheduling
Background
Karpenter does not see Savings Plans or Reserved Instances attached to your account. SP and RI are billing-layer discounts that aren't exposed through the EC2 API, so there's no way for them to reach Karpenter's Pricing Provider.
That leads to situations like this:
- You prepay a 1-year Compute SP for the m5 family → effectively ~30% off
- Karpenter looks at the public price and decides "m6i is a bit cheaper," so it provisions m6i
- Result: SP utilization drops, and m6i runs at the full On-Demand price
For reference, ODCR (On-Demand Capacity Reservation) and Capacity Block for ML have been natively recognized by Karpenter since v1. Those are "reserved capacity" and therefore surface through the EC2 API.
Correcting with NodeOverlay
Apply priceAdjustment: "-30%" to the m5 family that has the SP so that Karpenter "believes" m5 is cheap.
03-overlay-sp-discount.yaml
# Reflect the effective discount rate of the Savings Plan covering the m5 family apiVersion: karpenter.sh/v1alpha1 kind: NodeOverlay metadata: name: overlay-m5-sp-discount spec: weight: 10 requirements: - key: node.kubernetes.io/instance-type operator: In values: ["m5.large", "m5.xlarge", "m5.2xlarge"] - key: karpenter.sh/capacity-type operator: In values: ["on-demand"] priceAdjustment: "-30%"
Limitations
- Pricing vs. Billing are separate.
priceAdjustmentis only a scheduling hint and has no effect on your actual AWS invoice. The official docs are explicit about this: "Price adjustments influence Karpenter's scheduling decisions but don't affect actual cloud provider billing." - Coverage exhaustion isn't detected. Usage beyond the SP commit actually runs at the full On-Demand price, but the overlay will keep treating it as cheap.
- Manual management. You have to adjust the overlay value by hand whenever commitments renew or expire, or when you shift generations.
Instead of
priceAdjustment, you can also usespec.priceto override with an absolute value. The two fields are mutually exclusive, so pick one:priceAdjustmentfor discount-rate semantics,pricewhen you want to say "treat this instance's unit price as exactly X."
Scenario 2. Make Extended Resources Visible to the Scheduler
Background
Things like HugePages, smarter-devices/fuse, or custom Device Plugin resources (e.g. custom-hardware/accelerator) aren't part of the default instance metadata. So when a Pod requests one of these resources, Karpenter concludes:
"No instance type here can host this Pod."
And it either fails to provision, or scheduling ends up tangled in unexpected ways.
Correcting with NodeOverlay
Use the capacity field to tell Karpenter, "pretend this instance type has this Extended Resource." Karpenter then factors that into the simulation when picking candidate instances.
04-overlay-extended-capacity.yaml
# Tell the simulation that the m5 family has smarter-devices/fuse and hugepages-2Mi apiVersion: karpenter.sh/v1alpha1 kind: NodeOverlay metadata: name: overlay-extended-capacity spec: weight: 10 requirements: - key: node.kubernetes.io/instance-type operator: In values: ["m5.large", "m5.xlarge", "m5.2xlarge"] capacity: smarter-devices/fuse: 1 hugepages-2Mi: 100Mi
Heads up: Simulated ≠ Actual.
capacityonly affects Karpenter's scheduling simulation. It does not create those resources on the actual node. You still need to configure the real resources through a Device Plugin, a DaemonSet, HugePages in UserData, etc. Skip that step and you get the mismatch where Karpenter picks and launches the right node, but the Pod stays Pending with "insufficient resources."
Also, the
capacityfield is for Extended Resources only. You cannot override standard resources like CPU, memory, or storage.
Scenario 3. Apply a Soft Penalty to Specific Instances
Background
Sometimes you don't want to say "don't use this," but rather "prefer the other option first, and fall back to this one only if needed." For example:
- Protect GPU instances: if CPU workloads scale out and land on GPU nodes, the GPUs go to waste.
- Reflect license costs: when Windows or commercial DB licenses add a fixed per-instance cost, the real price is higher than the public price.
- Guide a generation transition: penalize the older family so workloads gradually move to the newer one.
You could achieve something similar with Taint/Toleration, but that's a binary "allowed/not allowed" control. A price-based penalty is handled as a priority, which is a better fit for expressing a soft preference.
Implementing with NodeOverlay
Set a positive value on priceAdjustment and the target instance looks more expensive, so Karpenter deprioritizes it.
05-overlay-soft-penalty.yaml
# Add a license cost to m6i.xlarge (absolute +0.50) apiVersion: karpenter.sh/v1alpha1 kind: NodeOverlay metadata: name: overlay-license-fee spec: weight: 5 requirements: - key: node.kubernetes.io/instance-type operator: In values: ["m6i.xlarge"] priceAdjustment: "+0.50" --- # Apply a +15% penalty to the older m5 family to nudge workloads toward m6i apiVersion: karpenter.sh/v1alpha1 kind: NodeOverlay metadata: name: overlay-legacy-family-penalty spec: weight: 5 requirements: - key: node.kubernetes.io/instance-type operator: In values: ["m5.large", "m5.xlarge", "m5.2xlarge"] priceAdjustment: "+15%"
Stacking Multiple Overlays: Conflict Resolution
When several overlays match the same instance type, the following rules apply:
- Higher
weightwins. Default is0; larger numbers take precedence. - Equal weights are broken by alphabetical order of the overlay name.
- Field-level behavior differs.
price/priceAdjustment: override (the higher-weight value replaces the lower-weight one)capacity: merge (capacity maps from different overlays are combined)
- Unresolvable conflicts (e.g. equal weight with different
priceAdjustmentvalues) surface asReady=Falseonstatus.conditions, and the overlay is not applied.
Worked example: SP discount + legacy penalty
overlay-m5-sp-discount from Scenario 1 (-30%) and overlay-legacy-family-penalty from Scenario 3 (+15%) both target the m5 family. Their weights are 10 and 5 respectively, so the SP discount wins and the legacy penalty is ignored. If you want both effects to land, you can't simply leave them at the same weight; combine them into a single priceAdjustment value (e.g. -19.5% as a rough -30% + +15% blend), or accept that one of the two intents is dropped.
Operational View: Consolidation and Change Propagation
NodeOverlay changes are automatically incorporated into Karpenter's consolidation logic.
- Price changes alter cost calculations, which can trigger replacement of existing nodes.
capacitychanges are also factored in when deciding whether workloads can be moved between nodes.- Updates roll out on the next consolidation cycle. No drift-forcing or manual node recreation is required.
Practical caution: if you already have many m5 nodes running and you add a fresh -30% to m5, Karpenter re-evaluates "m5 is now cheaper" and may replace some m6i nodes with m5. To avoid a large replacement wave, give consolidateAfter some headroom, or roll out weight changes gradually.
Debugging: Checking Status
You can verify whether an overlay is actually applied by inspecting its status:
kubectl get nodeoverlay kubectl describe nodeoverlay <name>
Key conditions:
Ready=True: the overlay is successfully applied to matching instance types.Ready=False: configuration conflicts or requirement mismatches prevented application. Checkstatus.conditionsforreason/message(e.g.reason: "Conflict",message: "conflict with another overlay").
Wrap-up
NodeOverlay lets you feed real-world context (discounts, licenses, custom resources) into Karpenter's price-based decision engine. If you need to customize how Karpenter chooses nodes, it's worth considering.
References
- Topics
- Containers
- Language
- English
Relevant content
AWS OFFICIALUpdated 2 years ago- asked 10 months ago
- asked a year ago
- Accepted Answerasked 2 years ago
AWS OFFICIALUpdated 10 months ago