Skip to main content

From Karpenter to GKE ComputeClasses: the mental model

vsz
Author
vsz
Product Manager on Google Kubernetes Engine, kicking tires in the cloud.

Part 1 of a 3-part series on porting EKS/Karpenter autoscaling patterns to GKE. Part 2 covers instance diversity and disruption control; Part 3 covers accelerators, reservations, and tenant isolation.

I’ll be honest up front: I’m not a Karpenter expert. My side of this is GKE — I’m the product manager for ComputeClasses at Google Cloud, so I know that model cold and I’m the first to admit I see the AWS side through the eyes of the multi-cloud teams bringing Karpenter patterns with them. And I spend a lot of time with those teams extending their footprint to GKE alongside EKS, where the same question comes up every time — “what’s my Karpenter here?” They’ve internalized a particular way of asking for compute on AWS, and they want the GKE equivalent for their second cloud, ideally without relearning autoscaling from scratch.

The short answer is ComputeClasses, and the mapping is cleaner than you’d expect. But there’s one conceptual shift to make first, and if you make it early the rest is mostly a translation exercise. This post is that shift, plus the two lookup tables we hand teams porting their provisioning patterns to GKE, plus one fully worked example — the cost-optimized Spot setup that’s probably 80% of why you picked Karpenter in the first place.

Two ways to say “give me nodes”
#

Here’s the shift, and I want to be straight about it rather than pretend the two tools are identical.

Karpenter asks you to describe a space. You hand a NodePool a broad requirements block — a family of instance types, a range of sizes, one or more capacity types — and Karpenter’s solver bin-packs your pending pods onto whatever instance it computes as the best fit right now. The expressiveness is the whole point: describe the feasible region, trust the optimizer to search it.

A ComputeClass asks you to declare a ranking. Instead of a space to search, you write an ordered list of priorities[] — a ranked expression of the tradeoffs you actually care about (reservation before on-demand before Spot; this family before that one; this zone policy). GKE’s autoscaler walks the list top-to-bottom and fast-fails to the next tier the moment one is unobtainable.

Karpenter hands a NodePool requirements block to a bin-pack solver that searches for one best-fit node; a ComputeClass instead walks an ordered priority list top-to-bottom, fast-failing past unobtainable tiers until on-demand n4 schedules

That’s a real design difference, not a marketing gloss — and it’s the one we made deliberately when we built ComputeClasses. You’re trading a solver’s search for an author’s ranking. Two properties fall out of that choice that I’d argue for even if I hadn’t helped design it:

  • It’s deterministic and auditable. You read the YAML and you know exactly what the autoscaler will try, and in what order. There’s no “why on earth did it pick that instance?” forensics after the fact — the answer is always “because it was the highest priority that was obtainable.” For a platform team defending a cloud bill, that legibility is the whole point, and it’s what we optimized the model for.
  • You still get flexibility — you just scope it per tier. A priority doesn’t have to name an exact SKU. Intent-based priorities (machineFamily: n4 plus minCores: 16, mixed architectures, locationPolicy: ANY) let GKE find the best-fit shape within that tier. So it isn’t “enumerate the whole catalog”; it’s “describe each tier by intent, then rank the tiers.”

The honest tradeoff: Karpenter’s In [a-long-list-of-instance-types] has no literal one-to-one. Collapsing that list into a short ranked set of intent-based priorities is the porting skill. Do it well and you keep almost all the flexibility while gaining a decision path you can actually reason about.

There’s a second thing people rightly value about Karpenter that I don’t want to gloss over: it’s open source, it’s a CNCF project, and that portability is part of the appeal. So the fair question is whether adopting ComputeClasses signs you up for GKE lock-in. I want to meet that head-on, because it’s a concern I hear a lot and one I take seriously in how we’re steering this. The short version — and I’ll come back to it properly in Part 3 — is that the direction of travel points the other way. At KubeCon EU 2026, Google announced it will open-source GKE Cluster Autoscaler, “one of the core components driving infrastructure provisioning,” with the explicit goal of “a vendor-neutral platform that the OSS community can benefit from and build on top of.” And the ComputeClass model is coming with it: Google intends to open-source ComputeClasses too — I’m driving that effort, and I’ll cover the details in Part 3. The upshot is that the API you’d standardize on here is heading toward vendor-neutral, not away from it. If parity with the Karpenter you’re still running on EKS is what’s making you hesitate to invest in learning ComputeClasses, hold that thought.

The concept mapping
#

Most Karpenter concepts have a direct GKE home. This is the table I keep open:

KarpenterGKE ComputeClassNote
NodePoolComputeClassCollapse multiple NodePools into one ranked priorities[].
spec.weightOrder in priorities[]Top wins; strictly ordered traversal.
requirements: instance-family In [...]machineFamily: per priorityOne family per priority; vary across priorities.
capacity-type: spotspot: trueDeclared per priority.
consolidateAfter: 30sconsolidationDelayMinutes: 1Floor is 1 minute.
driftactiveMigration: { optimizeRulePriority: true }Honors PodDisruptionBudgets.
disruption.budgetsWorkloadClass (or PDBs)More on this in Part 2 — it’s the interesting one.

That last row is the one worth flagging now, because it reflects a deliberate design stance. Karpenter bundles infra provisioning and workload disruption control into a single NodePool. On GKE we keep them separate: the ComputeClass decides where a workload runs, and a distinct, newer API — WorkloadClass — decides when it may be disrupted (maintenance windows, a minimum run duration before eviction is allowed, allowlisted disruption sources). Disruption tolerance is a property of the workload, not the infrastructure, so it belongs on the workload — and keeping ComputeClasses focused purely on provisioning is a big part of why the ranking model stays as legible as it does. I’ll dig into that in Part 2; just know the row exists.

Translating instance families
#

The other lookup table is hardware. GCP doesn’t have m6i, but it has a close cousin for every AWS class you’re likely running. The one thing to get right is the vendor axis — matching Intel-to-Intel and AMD-to-AMD keeps you aligned with any committed-use discounts you’re carrying over — and the generation axis, since GCP, like AWS, ships a current generation alongside a still-supported prior one.

AWS classGCP current-gen (→ prior-gen)
General purpose — m6i/m7i (Intel), m6a/m7a (AMD), m7g (Graviton)n4 Intel · n4d AMD · n4a/c4a Axion Arm — (prior gen: n2/n2d)
Performance / compute-leaning — c6i/c7i, c6a/c7a, c7gc4 Intel · c4d AMD · c4a Arm — (prior gen: c3/c3d)
HPC / compute-optimized — c5, hpc6ac2 / c2d, and h3 for tightly-coupled HPC
Burstable / cost-optimized — t3 / t4ge2 — the largest, cost-optimized (oversubscribed) general pool
Memory-optimized ~8 GB/vCPU — r6i / r7ithe -highmem variants of n4 / c4
Extreme memory, SAP HANA class — x2, u-*m4 (→ m3 / x4) — the memory-optimized M-series

Two of those rows trip people up, so they’re worth calling out:

  • e2 is the cost pool, not a peer of n4. It has a predefined CPU (Intel or AMD, chosen for you) and is oversubscribed — it’s the biggest, cheapest, most broadly available general pool, which makes it an excellent On-Demand availability floor, but it’s not the shape you reach for when you want dedicated, current-gen performance. That’s n4/n4d (or c4/c4d when you want more sustained throughput).
  • Memory-optimized splits in two. For ordinary memory-heavy workloads (the AWS r-series, ~8 GB/vCPU), stay on general-purpose and use the -highmem shapes of n4/c4. Reserve the true memory-optimized M-series (m4, m3, x4) for extreme ratios — the SAP HANA / large in-memory-database tier.

A couple of behavioral notes that also bite people coming from AWS:

  • Spot and On-Demand often share capacity for CPU on GCP. If On-Demand for a family is out in a zone, Spot for that family is frequently out too. So a Spot-only ComputeClass with no On-Demand floor can strand pods. Always end with an On-Demand floor — and e2 is a strong choice for it, precisely because it’s the largest pool. (Accelerators behave differently — Spot GPUs are often available when On-Demand isn’t — but that’s a Part 3 topic.)
  • Topology spread lives on the Pod, not the ComputeClass. Karpenter reads topologySpreadConstraints as part of its solve; on GKE you set them on the workload and the autoscaler respects them. Don’t go looking for a zone-spread knob in the ComputeClass spec.

A worked example: cost-optimized Spot with an On-Demand floor
#

Here’s the canonical Karpenter setup — one NodePool, capacity-type In [spot, on-demand], a wide instance-family list, consolidation on — and its ComputeClass translation. This is the pattern most teams want first.

apiVersion: cloud.google.com/v1
kind: ComputeClass
metadata:
  name: spot-cost-tiebreak-usc1
spec:
  nodePoolAutoCreation:
    enabled: true
  autoscalingPolicy:
    consolidationDelayMinutes: 2
    consolidationThreshold: 50
  priorityDefaults:
    location:
      zones: ['us-central1-a', 'us-central1-b', 'us-central1-c', 'us-central1-f']
      locationPolicy: ANY
  priorities:
  # Top tier: three interchangeable Spot families at the SAME score. The
  # autoscaler picks whichever is cheapest AND available right now.
  - machineFamily: e2
    minCores: 4
    spot: true
    priorityScore: 100
  - machineFamily: n2d
    minCores: 4
    spot: true
    priorityScore: 100
  - machineFamily: n4
    minCores: 4
    spot: true
    priorityScore: 100
  # On-Demand floor — guarantees execution if all Spot is exhausted.
  - machineFamily: e2
    minCores: 4
    spot: false
    priorityScore: 10
  whenUnsatisfiable: ScaleUpAnyway

Notice what priorityScore is doing here, because it’s the direct answer to “but I liked letting Karpenter choose the cheapest instance.” Priorities with the same score are evaluated together, and the tie-break is lowest unit cost available right now. So those three Spot families aren’t a fixed ordering — they’re a set, and GKE picks the cheapest one that has capacity this minute. That’s the “let it choose” behavior, aimed at the one decision where it actually pays off (Spot family selection, which would otherwise rot in your YAML as Spot prices shift). Below them, the On-Demand e2 floor at a lower score guarantees you never strand a pod.

A few rules worth internalizing from this one example:

  • Same score = a tie-break set (max 3 rules per score; requires GKE 1.35.2-gke.1842000+). Different scores = strict fallback order.
  • Spread the top tier across vendors/generations (cost-optimized e2, AMD n2d, current-gen Intel n4) purely for obtainability. The cost tie-break handles the picking, so the order among them doesn’t matter. And the On-Demand floor is e2 by design — it’s the largest, most broadly available pool, exactly what you want guaranteeing a scheduling path.
  • minCores is intent, not a pin. Node auto-creation sizes the node to your pods’ requests within that family — this is your Karpenter “right-sizing” behavior, and it needs no separate config. (More on diversity and sizing in Part 2.)

The one trap that will get you on day one
#

If you take nothing else from this post: translate your Pod node selectors.

This is the number-one failure teams hit when they bring their EKS pod specs over to GKE. Those pods carry nodeSelector / nodeAffinity keys that are AWS-style or generic — node.kubernetes.io/instance-type: m6i.4xlarge, or a home-grown machine-family: c4 label. GKE’s autoscaler doesn’t recognize those keys, no priority matches, and the pod sits Pending with a noScaleUp event. Nothing is broken; the autoscaler simply has nothing to match.

Three ways out, best last:

  • Machine family: generic machine-family: c4cloud.google.com/machine-family: c4.
  • Machine shape: AWS node.kubernetes.io/instance-type: m6i.4xlarge → GKE shape node.kubernetes.io/instance-type: n4-standard-16.
  • Best of all — stop selecting hardware and select the ComputeClass: nodeSelector: { cloud.google.com/compute-class: spot-cost-tiebreak-usc1 }. Let the priorities[] choose the family and shape. This is more idiomatic on GKE than pinning instance types on the Pod, and it’s exactly the decoupling that makes the ranking model pay off.

Where this series goes
#

That’s the mental model and your first working ComputeClass. Two more posts to come:

  • Part 2 — Diversity without a solver: expressing instance flexibility across families, sizes, and architectures; the size-obtainability escape hatch; and consolidation & drift — including where WorkloadClass gives you disruption control that Karpenter’s node-percentage budgets can’t (weekday freezes, minimum run durations, per-workload windows).
  • Part 3 — Accelerators, reservations, and isolation: GPU/TPU fallbacks (and the GPU toleration gotcha), reservation-first provisioning, tenant isolation, and Kueue for batch — and I’ll close the loop on portability, and where GKE’s open-sourcing of its autoscaling stack leaves the Karpenter-vs-ComputeClass parity question.

The ComputeClass patterns here come from an experimental GKE ComputeClasses agent skill I’ve been building — so if you’d rather have an agent write these for you than copy YAML out of a blog post, that’s an option too.