GKE node pools are not just about adding more machines; they’re about precisely tailoring the underlying infrastructure for specific workload needs.
Let’s see this in action. Imagine you have a batch processing job that needs high CPU and local SSDs for temporary storage, and a separate web service that needs consistent memory and network throughput. You’d create two distinct node pools.
Here’s how you’d create a node pool for your batch job using gcloud:
gcloud container node-pools create batch-pool \
--cluster=my-gke-cluster \
--machine-type=n1-standard-8 \
--num-nodes=3 \
--local-ssd-count=1 \
--enable-autoscaling --min-nodes=1 --max-nodes=10 \
--node-labels=workload=batch \
--node-taints=workload=batch:NoSchedule
And for your web service:
gcloud container node-pools create web-pool \
--cluster=my-gke-cluster \
--machine-type=e2-standard-4 \
--num-nodes=5 \
--enable-autoscaling --min-nodes=2 --max-nodes=20 \
--node-labels=workload=web \
--node-taints=workload=web:NoSchedule
The cluster flag specifies which GKE cluster this node pool belongs to. machine-type dictates the CPU and memory, n1-standard-8 gives you 8 vCPUs and 30GB RAM, while e2-standard-4 offers 4 vCPUs and 16GB RAM. num-nodes sets the initial count, and --local-ssd-count=1 attaches a 375GB local SSD to each node in the batch-pool. Autoscaling is configured with --min-nodes and --max-nodes to dynamically adjust the pool size.
Crucially, node-labels and node-taints are your primary tools for directing workloads. Labels are key-value pairs attached to nodes, and taints are applied to nodes to repel pods unless they have a matching toleration. In our example, workload=batch and workload=web are labels. The NoSchedule taint on each pool means that only pods tolerating that specific taint will be scheduled onto those nodes. For instance, your batch job pods would need tolerations: [{key: "workload", operator: "Equal", value: "batch", effect: "NoSchedule"}] in their YAML to land on the batch-pool. This ensures your high-CPU batch jobs don’t get starved by general-purpose web traffic, and vice-versa.
The mental model is that your GKE cluster is a pool of resources, and node pools are specialized subsets of those resources. You can have multiple node pools within a single cluster, each with its own characteristics: machine type, disk configuration, GPU availability, preemptible instances, and even specific GKE versions. This allows for fine-grained control over where your pods run, optimizing for cost, performance, and availability. When you create a pod, Kubernetes’ scheduler looks at its resource requests, tolerations, and node affinity/anti-affinity rules to decide which node, and thus which node pool, is the best fit.
When you create a new node pool, it doesn’t magically appear with the latest security patches and runtime versions. You need to manage the node image and Kubernetes version for each pool. If you don’t specify an --image-type or --cluster-version, GKE will use the defaults set by your cluster, which might not be what you want for a specialized pool. The gcloud container node-pools describe <pool-name> --cluster=<cluster-name> command will show you the current configuration.
The next concept to grapple with is managing node pool upgrades to avoid downtime.