Last modified November 26, 2025
Node pools
A node pool is a set of nodes within a Kubernetes cluster that share the same configuration (machine type, operating system, etc.). Each node in the pool is labeled by the node pool’s name.
Advantages
Prior to the introduction of node pools, a cluster could only comprise one type of worker node. The cluster would have to be scaled as a whole, and the availability zone distribution would apply to all worker nodes of a cluster. This would mean that every worker node would have to be big enough to run the largest possible workload, in terms of memory and CPU resources required. At the same time, all worker nodes in the cluster would have to use the same availability zone distribution, even if some workloads wouldn’t require the increased availability.
Node pools are independent groups of worker nodes belonging to a cluster, where all nodes within a pool share a common configuration. You can combine any type of node pool within one cluster. Node pools can differ regarding:
- Machine type
- Availability zone distribution
- Scaling configuration (number of nodes)
- Node labels (the pool name is added as node label by default)
Configuration
Node pools can be created, deleted or updated by changing the configuration used when creating the cluster
kubectl gs template cluster --provider capa --name mycluster \
--organization giantswarm \
--description "my test cluster" \
--machine-pool-name pool0 \
--machine-pool-min-size 3 \
--machine-pool-max-size 5 \
--machine-pool-instance-type r6i.xlarge
A node pool is identified by a name that you can pick as a cluster administrator. The name must follow these rules:
- must be between 5 and 20 characters long
- must start with a lowercase letter or number
- must end with a lowercase letter or number
- must contain only lowercase letters, numbers, and dashes
For example, pool0, group-1 are valid node pool names.
The node pool name will be a suffix of the cluster name. In the example above, the node pool name will be mycluster-pool0.
All nodes in the node pool will be labeled with the node pool name, using the giantswarm.io/machine-pool label. You can identify the nodes’ node pool using that label.
The example kubectl command below will list all nodes with role, node pool name, and node name.
kubectl get nodes \
-o=jsonpath='{range .items[*]}{.metadata.labels.kubernetes\.io/role}{"\t"}{.metadata.labels.giantswarm\.io/machine-pool}{"\t"}{.metadata.name}{"\n"}{end}' | sort
master ip-10-1-5-55.eu-central-1.compute.internal
worker mycluster-pool0 ip-10-1-6-225.eu-central-1.compute.internal
worker mycluster-pool0 ip-10-1-6-67.eu-central-1.compute.internal
In your workload cluster values, you can specify the node pool configuration to use Karpenter
global:
metadata:
name: test-cluster
organization: giantswarm
nodePools:
pool0:
type: karpenter # This is the key difference that will make karpenter manage your worker nodes
consolidateAfter: 6h
consolidationPolicy: WhenEmptyOrUnderutilized
consolidationBudgets: # You can control when and how nodes will be updated / rolled with the budgets
- nodes: "20%"
- schedule: "0 9 * * mon-fri"
duration: "8h"
nodes: "0"
reasons:
- "Drifted"
customNodeLabels:
- yourcustomlabel=canbeconfiguredhere
customNodeTaints:
- key: example.com/special-taint
effect: NoSchedule
expireAfter: 20h # The amount of time a Node can live on the cluster before being deleted by Karpenter
requirements: # In the 'requirements' you can specify which nodes you want karpenter to consider or to ignore for your node pool
- key: karpenter.k8s.aws/instance-cpu
operator: In
values:
- "4"
- "8"
- "16"
- "32"
- key: karpenter.k8s.aws/instance-hypervisor
operator: In
values:
- nitro
- key: kubernetes.io/arch
operator: In
values:
- amd64
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
- on-demand
- key: kubernetes.io/os
operator: In
values:
- linux
terminationGracePeriod: 30m # The amount of time a Node can be draining before Karpenter forcibly cleans up the node
limits: # Maximum amount of resources that the node pool can consume
cpu: 1000
memory: 1000Gi
providerSpecific:
region: "eu-west-1"
release:
version: 33.0.0
A node pool is identified by a name that you can pick as a cluster administrator. The name must follow these rules:
- must be between 5 and 20 characters long
- must start with a lowercase letter or number
- must end with a lowercase letter or number
- must contain only lowercase letters, numbers, and dashes
For example, pool0, group-1 are valid node pool names.
The node pool name will be a suffix of the cluster name. In the example above, the node pool name will be mycluster-pool0.
All nodes in the node pool will be labeled with the node pool name, using the giantswarm.io/machine-pool label. You can identify the nodes’ node pool using that label.
When using type: karpenter in your node pool configuration, Karpenter will be deployed in your cluster to manage your worker nodes.
The configuration values that you specify in the node pool configuration will be translated into the Custom Resources that Karpenter is watching: NodePools (nodepools.karpenter.sh) and EC2NodeClasses (ec2nodeclasses.karpenter.k8s.aws).
There will be a pair of these Custom Resources for every node pool that you define in your cluster values.
Both Karpenter Custom Resources NodePools and EC2NodeClasses offer many configuration options.
We expose most of those fields as values that you can set when defining your node pool in the cluster values.
Also, every EC2 instance that Karpenter is managing is represented by a Custom Resource called NodeClaim (nodeclaims.karpenter.sh).
In the Karpenter node pools you can’t specify the number of nodes in your cluster. Neither the minimum or maximum number of nodes that you want to have in your cluster. Instead, you can specify the maximum amount of resources that the node pool can consume.
Depending on the workloads that are deployed, Karpenter will try to optimize the number of nodes in your cluster. This process is called consolidation.
You can also configure your node pool to instruct Karpenter about when and how to do the consolidation through the disruption configurations.
kubectl gs template cluster --provider capz --name test-cluster \
--organization giantswarm \
--description "my test cluster" \
--machine-pool-name pool0 \
--machine-pool-min-size 3 \
--machine-pool-max-size 5 \
--machine-pool-instance-type Standard_D4s_v5
The node pool name will be a suffix of the cluster name. In the example above, the node pool name will be mycluster-pool0.
All nodes in the node pool will be labeled with the node pool name, using the giantswarm.io/machine-deployment label. You can identify the nodes’ node pool using that label.
The example kubectl command below will list all nodes with role, node pool name, and node name.
kubectl get nodes \
-o=jsonpath='{range .items[*]}{.metadata.labels.kubernetes\.io/role}{"\t"}{.metadata.labels.giantswarm\.io/machine-deployment}{"\t"}{.metadata.name}{"\n"}{end}' | sort
master mycluster-control-plane-34c45e6e-rrpnn
worker mycluster-pool0 mycluster-pool0-8268227a-56x9n
worker mycluster-pool0 mycluster-pool0-8268227a-hjf69
Assigning workloads to node pools
Knowing the node pool name of the pool to use, you can use the nodeSelector method of assigning pods to the node pool.
Assuming that the node pool name is pool0, and the cluster name is mycluster, your nodeSelector could for example look like this:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx
nodeSelector:
giantswarm.io/machine-pool: mycluster-pool0
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx
nodeSelector:
giantswarm.io/machine-pool: mycluster-pool0
A similar example for an Azure cluster:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx
nodeSelector:
giantswarm.io/machine-deployment: mycluster-pool0
You can assign workloads to node pools in a more indirect way too.
There is a set of labels that are automatically added by Kubernetes to all nodes, that you can use for scheduling.
For example: you have several node pools with different instance types. Using a nodeSelector with the label node.kubernetes.io/instance-type, you can assign workloads only to matching nodes.
Another example: you have different node pools using different availability zones. With a nodeSelector using the label topology.kubernetes.io/zone, you can assign your workload to the nodes in a particular availability zone.
Adding more node pools
You can add new node pools at any time. You just need to update the cluster configuration and specify the details of the node pools that you want to add.
For example, if this was the cluster configuration
metadata:
description: "my cluster"
name: test-cluster
organization: giantswarm
nodePools:
nodepool0:
maxSize: 4
minSize: 3
instanceTypeOverrides:
- r6i.xlarge
- r5.xlarge
- m5.xlarge
You can add a new node pool like this:
metadata:
description: "my cluster"
name: test-cluster
organization: giantswarm
nodePools:
nodepool0:
maxSize: 4
minSize: 3
instanceTypeOverrides:
- r6i.xlarge
- r5.xlarge
- m5.xlarge
nodepool1:
instanceType: m5.xlarge
maxSize: 3
minSize: 1
Updating an existing node pool
Warning: Please be aware that changing the name of a node pool will result in the deletion of the old node pool and the creation of a new one.
If you still want to change the name of a node pool, our recommendation is to add a new node pool with the new name. Then waiting for it to be healthy, and finally removing the old one.
Instances in the node pool will be rolled whenever these properties are changed in the node pool definition:
instanceTypeadditionalSecurityGroupscustomNodeLabels
Instances will also be rolled if these values are changed:
providerSpecific.ami
Instances in the node pool will be rolled whenever these properties are changed in the node pool definition:
additionalSecurityGroupscustomNodeLabelsexpireAfterterminationGracePeriod
Instances will also be rolled if these values are changed:
providerSpecific.ami
Instances in the node pool will be rolled whenever these properties are changed in the node pool definition:
instanceTypeadditionalSecurityGroupscustomNodeLabels
Instances will also be rolled if these values are changed:
providerSpecific.ami
What happens when a node pool is updated
On cluster update, nodes get replaced if their configuration changed (called “rolling nodes”). This means that pods running on the old nodes will be stopped and moved to new nodes automatically.
Nodes may also get replaced involuntarily, for example if the node becomes unhealthy (for example disk full, out of memory), the cloud provider has a hardware fault, or you are using AWS spot instances that can shut down at any time. Therefore, please make sure that your applications can handle pod restarts. This topic is too large to cover here. Our advise is to research “zero-downtime deployments” and “stateless applications” since with those best practices, typical applications survive pod restarts without any problems.
During a cluster upgrade, creating new EC2 instances (called “rolling nodes”) can be necessary. That only happens if anything changes in the node configuration, such as configuration files or newer version of Kubernetes or Flatcar Linux. For such planned node replacements, the default instance warmup settings to ensure that AWS doesn’t replace the old nodes too quickly all at once, but rather in steps so a human could still intervene if something goes wrong (for example roll back to previous version). One node will be replaced every 10 minutes for a small node pool. Instead, for bigger node pools, small sets of nodes would be replaced every 10 minutes.
When node pool instances need to be rolled, each instance receives a terminate signal from AWS. With aws-node-termination-handler preinstalled on the cluster, affected nodes are first gracefully drained before allowing AWS to finally continue terminating the EC2 instance. By default, the timeout for draining is global.nodePools.PATTERN.awsNodeTerminationHandler.heartbeatTimeoutSeconds=1800 (30 minutes). For nodes which could not be fully drained within that time, for example, because a pod did not terminate gracefully, the handler lets AWS continue the termination of the instance.
During a cluster upgrade, creating new EC2 instances (called “rolling nodes”) can be necessary. That only happens if anything changes in the node configuration, such as configuration files or newer version of Kubernetes or Flatcar Linux.
The disruption.budgets configuration of your node pool control the speed in which Karpenter can scale down nodes in your cluster.
When node pool instances need to be rolled, Karpenter automatically taints, drains, and terminates the nodes.
By setting the terminationGracePeriod field on your node pool, you can configure the amount of time a Node can be draining before Karpenter forcibly cleans up the node. Pods blocking eviction like PodDisruptionBudgets and do-not-disrupt will be respected during draining until the terminationGracePeriod is reached, where those pods will be forcibly deleted.
Node pool deletion
In a similar way, you can remove a node pool at any time by removing its configuration from the values and updating the cluster. When a node pool is deleted, all instances in the node pool will be terminated, and a similar process as described above will take place.
Using mixed instance types (only CAPA (AWS EC2))
On CAPA (AWS EC2), you can override the instance type of the node pool to enable mixed instances policy on AWS.
This can provide Amazon EC2 Auto Scaling with a larger selection of instance types to choose from when fulfilling node capacities.
You can set the instanceTypeOverrides value when defining your node pool in the cluster values. For example:
metadata:
description: "my cluster"
name: test-cluster
organization: giantswarm
nodePools:
nodepool0:
maxSize: 4
minSize: 3
instanceTypeOverrides:
- r6i.xlarge
- r5.xlarge
- m5.xlarge
Please notice that when setting instanceTypeOverrides, the instanceType value will be ignored, and the instance types defined in instanceTypeOverrides will be used instead.
Also, the order in which you define the instance types in instanceTypeOverrides is important.
The first instance type in the list that’s available in the selected availability zone will be used.
Note: Changing the instanceTypeOverrides value won’t trigger a rolling update of the node pool.
Using multiple instance types in a node pool has some benefits:
Together with spot instances, using multiple instance types allows better price optimization. Popular instance types tend to have more price adjustments. Allowing older-generation instance types that are less popular tends to result in lower costs and fewer interruptions.
Even without spot instances, AWS has a limited number of instances per type in each Availability Zone. It can happen that your selected instance type is temporarily out of stock just in the moment you are in demand of more worker nodes. Allowing the node pool to use multiple instance types reduces this risk and increases the likelihood that your node pool can grow when in need.
Node pools and autoscaling
With node pools, you set the autoscaling range per node pool. The Kubernetes cluster autoscaler has to decide which node pool to scale under which circumstances.
If you assign workloads to node pools as described above and the autoscaler finds pods in Pending state, it will decide based on the node selectors which node pools to scale up.
In case there are workloads not assigned to any node pools, the autoscaler may pick any node pool for scaling. For details on the decision logic, please check the upstream FAQ for AWS.
Pods in the cluster, and it will scale up the node pool based on the Pods that are in Pending state.}}
Not supported at the moment.
On-demand and spot instances
Node pools can make use of Amazon EC2 Spot Instances. On the node pool definition, you can enable it and select the maximum price to pay.
metadata:
description: "my cluster"
name: test-cluster
organization: giantswarm
nodePools:
pool0:
maxSize: 2
minSize: 2
spotInstances:
enabled: true
maxPrice: 1.2
Node pools may use Amazon EC2 Spot Instances. On the node pool definition, you can enable it in the requirements field
global:
metadata:
name: test-cluster
organization: giantswarm
nodePools:
pool0:
type: karpenter
requirements:
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
- on-demand
providerSpecific:
region: "eu-west-1"
release:
version: 33.0.0
Automatic termination of unhealthy nodes (machine health checks)
Degraded nodes in a Kubernetes cluster should be a rare issue, however when it occurs, it can have severe consequences for the workloads scheduled to the affected nodes. The goal should be to detect bad nodes early and remove them from the cluster, replacing them with healthy ones.
The node’s health status is used to determine if a node needs to be terminated. A node reporting a Ready status is one sign for considering it healthy. But it may also have other problems, such as a full disk. If a node reports an unhealthy status continuously for a given time, and the MachineHealthCheck is enabled, it will be recycled.
For tuning the behavior of automatic node termination on AWS, you can configure your cluster-aws App with these values (see documentation):
global:
# For worker nodes, the check is *disabled* by default
nodePools:
pool0:
type: machinepool # not yet supported for Karpenter pools
minSize: 3
maxSize: 15
machineHealthCheck:
# This is the only required field, since the check is disabled by default
# for workers
enabled: true
# maxUnhealthy says to not delete any nodes if more than this value
# (in this example: 20%; for 15 nodes that would be 3 nodes) are unhealthy.
# This is a safety guard in case many nodes become unhealthy at the same
# time. Deleting them would exacerbate the problem or even bring all
# workloads down. Please adjust the value according to your pool size.
maxUnhealthy: 20%
# Checks using built-in status fields of Node objects.
# All of these are enabled by default with reasonable defaults,
# i.e. the time until a node gets deleted if it stays in this bad condition.
# We recommend to use the defaults (don't set the fields at all):
nodeStartupTimeout: 8m0s
unhealthyNotReadyTimeout: 10m0s
unhealthyUnknownTimeout: 10m0s
# Optional, these enable custom checks for certain disks, using
# the component "node-problem-detector". Disabled by default if you don't
# set any of these fields.
diskFullContainerdTimeout: "15m"
diskFullKubeletTimeout: "15m"
diskFullVarLogTimeout: "15m"
For control plane nodes, the check is enabled by default and we recommend leaving the defaults. If you still want to customize, the same fields as shown above are available as global.controlPlane.machineHealthCheck.
Limitations
- Clusters without worker nodes (= without node pools) can’t be considered fully functional. In order to have all required components scheduled, worker nodes are required. For that reason, it’s deactivated any monitoring and alerts for these clusters and don’t provide any proactive support.
Learn more about how to assign Pods to Nodes from the official documentation.
Need help, got feedback?
We listen to your Slack support channel. You can also reach us at support@giantswarm.io. And of course, we welcome your pull requests!