Karpenter Node Provisioning on AWS
Turn Ziba's unschedulable pods into right-sized EC2 nodes without confusing pod scaling with machine provisioning.
The enterprise problem and today’s slice
Enterprise problem: KEDA can request ten Ziba workers, but if their memory requests cannot fit existing nodes they remain Pending while the publishing backlog grows; a broad fixed node group may solve the incident by buying months of idle RAM. Whole-course context: The incoming evidence is a bounded KEDA/HPA replica target plus Pending reasons, pod requests, affinity, and tolerations; today maps those constraints to Amazon Elastic Compute Cloud (EC2) capacity. Today’s slice: We configure Karpenter NodePool and AWS EC2NodeClass, follow the resulting NodeClaim to a virtual machine and physical host, and bound disruption. End-of-day evidence: A run correlates Pending pod UID, NodeClaim, EC2 instance ID, registered Node, workload start, consolidation, denial path, cost tags, and timestamps. Still unsolved: A provider-neutral on-premises versus AWS versus Google Cloud platform decision remains for the capstone.
The thesis is that Karpenter does not resize a pod: it turns unschedulable pod constraints into a new node lifecycle. The smallest complete model is Pending demand, a provisioning decision, and an actual machine that registers with Kubernetes.
Customer use cases
The customer needs publishing capacity, not merely an EC2 API success, so machine creation must end in a running workload. A second use case proves that policy rejects incompatible, over-budget, or unauthorised supply.
| Use case ID | Actor | Customer job | Success outcome | Denial or recovery evidence |
|---|---|---|---|---|
| D67-UC-01 | Ziba platform operator | Place memory-justified render workers that do not fit current nodes | Karpenter launches a compatible instance, kubelet registers a Node, workers bind, and queue age falls | If EC2 capacity is unavailable, alternate allowed offerings are attempted and the unresolved pod remains visibly Pending |
| D67-UC-02 | Cloud governance engineer | Bound which AWS machines, networks, identities, and purchase options Karpenter may create | Every instance matches approved NodePool constraints, EC2NodeClass selectors, IAM role, encryption, tags, and aggregate limits | A pod demanding forbidden hardware or zone remains Pending with an explainable incompatibility; unaffected approved work still schedules |
Actor-centred user stories
Dynamic provisioning is unsafe when application teams can indirectly create arbitrary machines, so acceptance includes both placement and governance. These stories make the Kubernetes request, AWS resource, and physical consequence traceable.
| Story ID | Use case IDs | User story | Observable acceptance conditions |
|---|---|---|---|
| D67-US-01 | D67-UC-01 | As a Ziba platform operator, I want Pending worker constraints translated into right-sized EC2 capacity, so that the publishing backlog drains without maintaining a permanently oversized fleet | Evidence links scheduler reason, pod requests, selected offering, NodeClaim, instance ID, Node UID, pod binding, queue drain, and consolidation outcome |
| D67-US-02 | D67-UC-02 | As a cloud governance engineer, I want provisioning constrained by declarative policy, so that a workload cannot escape approved cost, network, identity, architecture, or disruption boundaries | A forbidden request is denied or remains unsatisfied, approved work is a positive control, and CloudTrail/Kubernetes audit evidence identifies actor, policy revision, resource, time, and environment |
End-to-end product flows
Provisioning crosses two control planes—Kubernetes and AWS—so an EC2 instance is only an intermediate result. The happy path ends when the pod runs; the denied path ends with an explainable constraint mismatch and no unauthorised machine.
| Flow ID | Use case IDs | Path | Trigger | Numbered steps | Terminal evidence |
|---|---|---|---|---|---|
| D67-FLOW-01 | D67-UC-01 | Happy | KEDA/HPA creates workers that the scheduler marks Unschedulable=True | 1. Karpenter batches Pending pod requirements. 2. It intersects pod constraints with eligible NodePools. 3. It resolves AWS settings from the referenced EC2NodeClass. 4. It creates a NodeClaim and calls EC2 Fleet. 5. EC2 boots an instance in a selected subnet with security groups, AMI, storage, and IAM role. 6. Kubelet registers the Node. 7. Scheduler binds pods. 8. After demand falls, Karpenter safely consolidates under budgets. | Pod UID, constraints, NodePool generation, NodeClaim UID, offering, instance ID, Node UID, bind time, customer queue result, disruption decision, and immutable run ID |
| D67-FLOW-02 | D67-UC-02 | Denied | Test pod requests a forbidden GPU label and disallowed zone | 1. Scheduler marks the pod unschedulable. 2. Karpenter evaluates all NodePools. 3. No allowed requirement set satisfies the pod. 4. No NodeClaim or EC2 instance is created for it. 5. Alert records incompatibility. 6. An approved worker schedules as a positive control. | Denied pod UID, incompatible requirement message, zero matching NodeClaims/instances, policy revision, approved pod and node IDs, environment, timestamp, and test ID |
System design derived from the flows
Node provisioning fails when Kubernetes policy and AWS configuration drift apart, because neither control plane alone proves a usable Node. The design follows the request from scheduler evidence through Karpenter state to EC2, networking, storage, IAM, and kubelet registration.
| Use case ID | Entry point | Responsible services | Authoritative store | Failure evidence |
|---|---|---|---|---|
| D67-UC-01 | Pending pod and Karpenter reconciliation | Kubernetes scheduler, Karpenter controller, NodePool/NodeClaim controllers, AWS EC2 Fleet, kubelet | Kubernetes API for Pods, NodePools, NodeClaims, Nodes; AWS APIs for instance, subnet, security group, volume, image, and capacity state | Unschedulable reason, NodeClaim condition, EC2 insufficient-capacity/error code, bootstrap failure, unregistered Node, or unbound pod |
| D67-UC-02 | Git-reviewed NodePool and EC2NodeClass policy | GitOps controller, Kubernetes admission/RBAC, Karpenter, AWS IAM, EC2, CloudTrail and cost allocation | Git commit plus Kubernetes desired state; AWS IAM and resource state; audit/evidence stores | Admission denial, incompatible requirements, IAM access denial, aggregate NodePool limit, disruption-budget block, or missing mandatory tag |
Kubernetes documentation distinguishes Cluster Autoscaler, which adds nodes to preconfigured node groups, from Karpenter, which auto-provisions nodes from NodePool constraints and manages their lifecycle (Kubernetes node autoscaling). Karpenter’s official model is NodePool for portable constraints, NodeClass for provider-specific configuration, and NodeClaim for one requested node lifecycle (Karpenter concepts).
Data model and ownership
Deleting a Kubernetes Node object is not equivalent to proving that its billed EC2 instance and disks disappeared, so lifecycle records must cross-reference without merging authority. Kubernetes owns scheduling intent; AWS owns the infrastructure resources and billing state.
Generated-application database: Not created in this slice — Kubernetes and AWS control planes plus Git and evidence storage already own the durable provisioning, policy, audit, and lifecycle records.
| Record or entity | Store and owner | Primary key | Foreign key or opaque reference | Tenant key | Material constraint | Lifecycle and deletion | Use case IDs |
|---|---|---|---|---|---|---|---|
| NodePoolRevision | Git and Kubernetes API, owned by platform engineering and Karpenter | NodePool UID plus generation | Local EC2NodeClass name | Cluster identifier | Requirements, aggregate limits, taints, expiration, and disruption budgets must pass review and admission | Created from Git, revised declaratively, deletion cascades only according to controller semantics; audit retained | D67-UC-01, D67-UC-02 |
| EC2NodeClassRevision | Git and Kubernetes API, owned by cloud platform and Karpenter AWS provider | EC2NodeClass UID plus generation | Opaque AWS role, subnet, security-group, AMI, and instance-profile references | AWS account plus cluster identifier | Discovery selectors must resolve approved private networking, IAM, image, encrypted storage, and tags | Revised through Git; deletion blocked or sequenced until dependent NodeClaims retire; audit retained | D67-UC-01, D67-UC-02 |
| NodeClaimLifecycle | Kubernetes API, owned by Karpenter | NodeClaim UID | Local NodePool owner reference plus opaque provider instance ID | Cluster identifier | One claim represents one node request and must satisfy both pod and pool constraints | Created for provisioning, transitions launch/register/initialize, drained and finalised before removal | D67-UC-01, D67-UC-02 |
| EC2InstanceLifecycle | AWS EC2 and CloudTrail, owned by cloud platform | EC2 instance ID | Opaque NodeClaim and Kubernetes Node UID tags | AWS account plus cluster identifier | Approved instance offering, subnet, security groups, IAM role, AMI, encrypted volumes, and cost tags | Launched, billed, stopped only if policy permits, terminated after drain; audit retained independently | D67-UC-01, D67-UC-02 |
| PlacementAndDisruptionEvidence | Kubernetes status/events and evidence store, owned by SRE | Run ID plus event timestamp | Pod UID, NodeClaim UID, Node UID, and opaque instance ID | Ziba environment | Positive and negative paths must share policy revision and observation window | Written per test/incident, immutable for retention period, then expired by evidence policy | D67-UC-01, D67-UC-02 |
Karpenter documents that each NodeClaim has an owner reference to its NodePool and a termination finalizer used to coordinate draining, provider-instance termination, and Node cleanup (Karpenter NodeClaims). Always verify the installed controller version’s exact lifecycle behavior before destructive tests.
Declare current Karpenter v1 resources
An unbounded NodePool can turn one malformed pod into expensive or privileged infrastructure, so constrain architecture, capacity type, family, generation, zones, and aggregate resources. This current v1 example uses a portable NodePool and AWS-specific EC2NodeClass; replace discovery tags and role with values created for the cluster.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: ziba-general
spec:
template:
metadata:
labels:
workload.ziba.dev/tier: general
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: ziba-general
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"]
- key: kubernetes.io/os
operator: In
values: ["linux"]
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["5"]
- key: topology.kubernetes.io/zone
operator: In
values: ["eu-west-2a", "eu-west-2b", "eu-west-2c"]
expireAfter: 720h
limits:
cpu: "200"
memory: 800Gi
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 5m
budgets:
- nodes: "20%"
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: ziba-general
spec:
role: KarpenterNodeRole-ziba-production
amiSelectorTerms:
- alias: al2023@latest
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: ziba-production
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: ziba-production
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 80Gi
volumeType: gp3
encrypted: true
deleteOnTermination: true
metadataOptions:
httpEndpoint: enabled
httpProtocolIPv6: disabled
httpPutResponseHopLimit: 1
httpTokens: required
tags:
application: ziba
environment: production
owner: platform
For reproducible production rollouts, resolve and pin an approved immutable AMI alias rather than leaving @latest in Git. Karpenter requires each NodePool to reference a NodeClass; the AWS EC2NodeClass controls AMI discovery, subnet and security-group selection, node role or instance profile, storage, metadata options, and tags (Karpenter NodePools, Karpenter EC2NodeClasses).
| Code declaration | Kubernetes/software effect | AWS VM or physical effect |
|---|---|---|
requirements | Filters NodePools and offerings that may satisfy Pending pods | Restricts EC2 architecture, family, generation, zone, and purchase options that can be launched |
limits.memory: 800Gi | Bounds aggregate managed NodePool capacity, subject to distributed reconciliation | Prevents intended supply from growing beyond the declared memory ceiling; it is not a billing budget guarantee |
EC2NodeClass selectors | Resolves AWS launch configuration | Chooses VPC subnets, security groups, AMI, IAM role, and EBS attached to each VM |
encrypted gp3 mapping | Generates launch-template block-device configuration | EC2 attaches an encrypted 80 GiB EBS root volume that stores OS, images, and ephemeral container data |
metadataOptions.httpTokens: required | Requires IMDSv2 tokens for instance metadata access | Changes the EC2 instance metadata service exposure, not Kubernetes RBAC |
| consolidation settings | Karpenter may cordon, drain, replace, or remove eligible nodes within budgets | EC2 instances and billed capacity may terminate only after workload disruption constraints are evaluated |
Follow one Pending pod to silicon
A NodeClaim marked launched is not customer capacity until kubelet registers and the pod runs, so trace every transition. The final physical host is AWS-owned, while the EC2 VM, EBS volume, elastic network interface, and workload are customer-account resources with separate lifecycle evidence.
Karpenter reasons from pod requests and scheduling constraints, not actual post-start memory usage. A worker with a 1 GiB request and 4 GiB real peak can still trigger node pressure after placement; first repair truthful requests and limits, then let node provisioning respond to them.
Observe, falsify, and recover
Cloud consoles alone cannot explain why Kubernetes wanted a machine, while Kubernetes alone cannot prove whether EC2 billed or terminated it. Correlate both control planes and retain the identifiers.
kubectl get pods -A --field-selector=status.phase=Pending
kubectl describe pod -n ziba ziba-render-worker-example
kubectl get nodepools,nodeclaims
kubectl describe nodepool ziba-general
kubectl describe ec2nodeclass ziba-general
kubectl get nodes -L karpenter.sh/nodepool,karpenter.sh/capacity-type,node.kubernetes.io/instance-type
aws ec2 describe-instances --filters Name=tag:karpenter.sh/nodepool,Values=ziba-general
| Command | What it reads in software | What it proves about hardware or billing |
|---|---|---|
describe pod | Scheduler constraints and failure events | Identifies the demand Karpenter should satisfy; no machine proof yet |
get nodeclaims | Per-node provisioning intent and conditions | Provider ID links toward one EC2 instance, but registration must still be verified |
describe ec2nodeclass | Resolved readiness of AMI, subnets, security groups and identity | Shows launch prerequisites, not that an instance is healthy |
get nodes -L ... | Registered Node, instance type and capacity type labels | Kubelet on a VM reached the API and reported allocatable resources |
aws ec2 describe-instances | AWS instance lifecycle, placement, tags, interfaces and volumes | Confirms an account-billed EC2 resource exists; correlate its ID back to NodeClaim and Node |
The denied test should request a label or zone outside all approved requirements, then assert that no matching NodeClaim or EC2 instance appears. Never test denial by granting broader IAM rights or allowing an unbounded NodePool.
Karpenter versus Cluster Autoscaler
Choosing by popularity obscures the real design decision: whether capacity comes from predeclared groups or is synthesized from workload constraints. Both respond to Pending pods and both require truthful requests, cloud permissions, disruption policy, and observation.
| Decision | Cluster Autoscaler | Karpenter on AWS |
|---|---|---|
| Supply model | Changes desired size of configured node groups | Creates NodeClaims and selects compatible EC2 offerings from NodePool constraints |
| Instance choice | Bounded by each node group’s configured template/types | Can choose among many allowed types, zones, architectures, and capacity types per provisioning decision |
| Provider reach | Kubernetes project integrations for multiple clouds | Provider-specific; this day uses the AWS provider and EC2NodeClass |
| Lifecycle | Adds/removes nodes through group scaling | Provisioning plus consolidation, drift, expiration, and replacement controls |
| Prefer when | Standardised node groups and cross-provider operational consistency are more valuable | AWS workload diversity and just-in-time right-sizing justify the controller and IAM complexity |
| Avoid when | Group count explodes to represent every shape | The environment is not supported, direct EC2 permissions are unacceptable, or simpler managed compute meets the need |
Amazon EKS managed node groups provision nodes as EC2 instances in Auto Scaling groups and automate node lifecycle operations (AWS EKS managed node groups). Karpenter is not a replacement for KEDA or HPA: demand first creates pods; only unschedulable pods create node demand.
Failure modes and recovery order
Provisioning retries can amplify cost and delay when the root cause is an impossible pod constraint or broken bootstrap. Recover from the earliest failing boundary instead of repeatedly widening NodePool policy.
| Symptom | Boundary | First evidence | Recovery decision |
|---|---|---|---|
| No eligible NodePool | Kubernetes policy | Karpenter incompatibility logs/events | Correct unjustified pod constraint or add a reviewed, bounded pool; do not silently relax all restrictions |
| Eligible pool but no offering | AWS capacity | NodeClaim/EC2 capacity error | Permit approved alternate types/zones/capacity type or retain buffer for latency-sensitive work |
| Instance launched, Node absent | Bootstrap/network/IAM | EC2 console output, kubelet logs, security route and IAM evidence | Repair AMI/user data, endpoint reachability, DNS, time, or node role; terminate leaked instance safely |
| Node registered, pod still Pending | Kubernetes scheduling | Scheduler events, taints, affinity, volumes | Repair the remaining constraint; another node may not help |
| Pod runs then OOMs | Container cgroup and application | OOMKilled, usage profile, node pressure | Fix leak or justified request/limit; provisioning alone cannot raise the cgroup ceiling |
| Consolidation disrupts service | Workload/disruption policy | eviction, PodDisruptionBudget and Karpenter event | Tighten budgets, availability topology and termination grace; pause disruption while preserving provisioning |
The reusable rule is: a Pending pod asks for a node; a NodePool limits acceptable answers; an EC2NodeClass explains how AWS builds one; a NodeClaim tracks one attempt; a registered Node with a running pod proves delivered capacity.
Key takeaways
Dynamic nodes are safe only when workload intent, cloud supply, and physical runtime evidence remain connected. Karpenter expands the machine layer after pod scaling—it does not replace workload autoscaling.
NodePooldeclares portable constraints and lifecycle policy;EC2NodeClassdeclares AWS launch details.NodeClaimrepresents one requested node and links Kubernetes intent to a provider instance.- EC2 creates a VM backed by AWS physical hardware; kubelet registration turns that VM into a Kubernetes Node.
- Karpenter uses requests and scheduling constraints, so dishonest memory requests produce dishonest machine choices.
- NodePool limits, IAM, network selectors, immutable AMIs, encrypted disks, tags, and disruption budgets are governance controls.
- Cluster Autoscaler scales preconfigured groups; Karpenter auto-provisions from allowed constraints.
Checklist
A production NodePool is not ready merely because a test instance launched. Review the complete customer-to-hardware and deletion path.
- [ ] Pending reasons and resource requests are justified by observed workload evidence.
- [ ] NodePools are mutually understandable, bounded, and do not overlap accidentally.
- [ ] EC2NodeClass resolves only approved subnets, security groups, IAM role, immutable AMI, encrypted storage, and tags.
- [ ] Karpenter controller permissions are least-privilege and CloudTrail-visible.
- [ ] A successful test correlates pod, NodeClaim, instance, Node, workload, and customer outcome.
- [ ] A forbidden constraint creates no unauthorised NodeClaim or EC2 instance.
- [ ] Node bootstrap, leaked-instance detection, interruption, consolidation, and rollback are rehearsed.
- [ ] PodDisruptionBudgets and Karpenter disruption budgets preserve the required availability.
- [ ] Cost alerts and aggregate capacity limits cover reconciliation races and unexpected demand.