The EC2NodeClass defines the infrastructure side: AMI, security groups, subnets, instance profile, and userData. This episode covers multi-subnet customization, additional tags, and combining multiple NodeClasses.

In episode 4 you understood how the NodePool determines what is allowed to be scheduled via requirements, disruption, and weight. But a NodePool doesn't work alone — every launched node needs infrastructure configuration: which AMI, which subnet, which security group, and what role is attached. All of that is handled by EC2NodeClass.
This episode dissects EC2NodeClass thoroughly: AMI selection, security groups, subnets, instance profile, userData, additional tags, and how to combine multiple NodeClasses for different workloads.
EC2NodeClass is an AWS-owned CRD (karpenter.k8s.aws/v1) that answers the question where and how nodes are created. It contains all the AWS details needed to launch an instance:
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
amiFamily: AL2023
role: "KarpenterNodeRole-eks-learn-karpenter"
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: learn-karpenter
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: learn-karpenter
tags:
Environment: production
ManagedBy: karpenter
userData: |
#!/bin/bash
sysctl -w fs.inotify.max_user_instances=1024Each NodePool points to one EC2NodeClass via nodeClassRef. One EC2NodeClass can be used by many NodePools — for example, an on-demand pool and a Spot pool sharing the same infrastructure.
Karpenter selects an AMI through two mechanisms: amiFamily and amiSelectorTerms.
amiFamily provides AMIs that are managed and bootstrapped automatically by Karpenter. Available options:
| amiFamily | Distribution | Suitable for |
|---|---|---|
| AL2 | Amazon Linux 2 | Default, most stable |
| AL2023 | Amazon Linux 2023 | Modern, default since recent versions |
| Bottlerocket | Bottlerocket OS | Maximum security, immutable |
| Ubuntu | Ubuntu LTS | Tooling compatibility |
| Custom | Your own AMI | Special needs |
With amiFamily, Karpenter manages the kubelet bootstrap userData automatically — you just add your custom parts via the userData field.
For full control, use amiSelectorTerms. Karpenter searches for AMIs matching an alias or tags. Example using an alias for a Bottlerocket version:
spec:
amiSelectorTerms:
- alias: bottlerocket@latestSelectors can be based on alias, name, id, or tags. Combining amiFamily: Custom with a tag selector is the most common way to use corporate internal AMIs.
Karpenter discovers security groups and subnets via selector terms based on tags. This replaces the need to list IDs one by one.
spec:
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: learn-karpenter
env: prod
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: learn-karpenterA subnet selector can select many subnets at once — this is how Karpenter gains Availability Zone flexibility. During launch, Karpenter picks the AZ and subnet with sufficient capacity:
spec:
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: learn-karpenter
- tags:
karpenter.sh/discovery: data-planeThe two selectors above are combined with OR logic: a subnet holding either tag will be selected. The result is nodes spread across several AZs, and resilience to zone outages improves.
Tip
Want to ensure nodes are always born in a specific zone? Add a topology.kubernetes.io/zone requirement in the NodePool, or narrow the subnet selector. Remember: the NodePool controls zones, the EC2NodeClass controls which subnets are available.
The role field on an EC2NodeClass points to the instance profile IAM attached to every node. This role is what gives kubelet, CNI, and workloads permission to talk to AWS.
spec:
role: "KarpenterNodeRole-eks-learn-karpenter"This role is usually built alongside eksctl or Terraform, and differs from the Karpenter controller role we created in episode 3 — one is for Karpenter's control plane, the other for the nodes.
userData lets you inject scripts or cloud-config at instance boot time, on top of the default userData generated by Karpenter. On Amazon Linux, just use a bash shebang:
spec:
userData: |
#!/bin/bash
echo "EBS-optimized node" > /etc/motd
sysctl -w fs.inotify.max_user_watches=524288
systemctl restart kubeletKarpenter merges custom userData with its default bootstrap automatically. On Bottlerocket, the format uses TOML — make sure you use the format matching the chosen amiFamily.
The tags field adds EC2 tags to every instance, volume, and network interface created. This is very useful for cost tagging:
spec:
tags:
Environment: production
Team: platform
CostCenter: cc-1201These additional tags appear in AWS billing and make cost reports per team or per environment easier.
The NodePool + EC2NodeClass combination is very flexible. A real-world example: one cluster with three different node profiles.
| NodeClass | amiFamily | userData | Used by NodePools |
|---|---|---|---|
| default | AL2023 | no tuning | general, spot |
| gpu | Bottlerocket | Nvidia tuning | gpu-pool |
| hardened | Custom | CIS hardening | security-pool |
One default EC2NodeClass can serve two different pools (on-demand and Spot), while GPU workloads use their own NodeClass with Bottlerocket and special tuning. Example NodeClass for the GPU pool:
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: gpu
spec:
amiFamily: Bottlerocket
role: "KarpenterNodeRole-eks-learn-karpenter"
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: learn-karpenter
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: learn-karpenter
amiSelectorTerms:
- alias: bottlerocket@latest
tags:
Team: mlImportant
Make sure the instance-type requirement in the NodePool matches the actual capacity available — the gpu NodePool must use instance types like g5 or p4d so Karpenter doesn't try to launch GPU instances from the wrong pool. Inconsistent NodePool and NodeClass combinations are the most common source of bugs.
Quick verification: kubectl get ec2nodeclasses for the list, and kubectl describe ec2nodeclass gpu for the details.
Episode 5 completes the foundation of Karpenter configuration on AWS. EC2NodeClass handles the infrastructure side: AMI selection via amiFamily and amiSelectorTerms, security groups and subnets via tag selectors, instance profile via the role field, userData for custom bootstrap, and additional tags for cost. Combining several NodeClasses with several NodePools gives full control over the node profiles in a cluster.
Key takeaways:
In episode 6 we'll move into the second phase: provisioning and binpacking. We'll see how Karpenter combines queued pods into the most densely packed and cost-effective instances. See you in the next episode!