The first hands-on episode: create an EKS cluster, install the CRDs and Karpenter controller via Helm, build an IAM role on the least-privilege principle, then verify the installation with minimal NodePool and EC2NodeClass examples.

In episode 2 you understood Karpenter's architecture: a controller that detects Unschedulable pods, selects a NodePool, then launches nodes via the cloud provider. Now it's time for real practice. This episode guides you from zero — creating an EKS cluster, installing the CRDs and Karpenter controller via Helm, building a minimal IAM role, all the way to verification with your first NodePool and EC2NodeClass examples.
Make sure the tools from episode 0 are installed and your AWS credentials are valid. Let's start with cluster creation.
Karpenter 1.14.0 supports Kubernetes 1.29 to 1.36. Create a cluster with one managed nodegroup that acts as a vended nodegroup — the term for eksctl's built-in nodegroup that delivers a usable cluster before Karpenter takes over. This nodegroup is intentionally kept small and only runs system pods.
eksctl create cluster \
--name learn-karpenter \
--version 1.32 \
--region ap-southeast-1 \
--nodegroup-name system \
--node-type m5.large \
--nodes 1 \
--nodes-min 1 \
--nodes-max 1 \
--managedThe process takes 10-15 minutes. Once finished, make sure the cluster is healthy:
kubectl cluster-info
kubectl get nodesKarpenter calls the AWS API to launch and terminate instances. The IAM role for the controller is created on the least-privilege principle and attached via IRSA (IAM Roles for Service Accounts).
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:RunInstances",
"ec2:CreateFleet",
"ec2:TerminateInstances",
"ec2:DescribeInstances",
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups",
"ec2:DescribeInstanceTypes"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": ["iam:PassRole"],
"Resource": "arn:aws:iam::ACCOUNT_ID:role/KarpenterNodeRole-*"
}
]
}Create the policy via the aws CLI, then attach it to the service account:
aws iam create-policy \
--policy-name KarpenterControllerPolicy \
--policy-document file://policy.json
eksctl create iamserviceaccount \
--cluster learn-karpenter \
--namespace karpenter \
--name karpenter \
--role-name karpenter-irsa \
--attach-policy-arn arn:aws:iam::ACCOUNT_ID:policy/KarpenterControllerPolicy \
--approveWarning
Make sure the policy.json file uses the correct account number, and never put AWS access keys inside the cluster. IRSA ensures credentials are managed by IAM, not stored as secrets in Kubernetes.
Nodes launched by Karpenter also need an instance profile with a role to communicate with EKS — this role is usually named KarpenterNodeRole-... . In episode 5 we'll see how EC2NodeClass points to this instance profile via the role field.
The karpenter Helm chart installs the controller along with the NodePool, NodeClaim, and EC2NodeClass CRDs.
helm upgrade --install karpenter oci://ghcr.io/aws/karpenter/charts/karpenter \
--namespace karpenter \
--create-namespace \
--set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=arn:aws:iam::ACCOUNT_ID:role/karpenter-irsa \
--set controller.resources.requests.cpu=1 \
--set controller.resources.requests.memory=1Gi \
--version 1.14.0Verify the controller and CRDs:
kubectl get pods -n karpenter
kubectl get crd nodepools.karpenter.sh
kubectl get crd nodeclaims.karpenter.sh
kubectl get crd ec2nodeclasses.karpenter.k8s.awsA healthy controller is marked by pods in Running status. To confirm the version, check the image section on the deployment kubectl get deployment -n karpenter karpenter -o yaml — it should be tagged 1.14.0.
Before creating Karpenter resources, make sure the subnet and security group can be "discovered" by Karpenter. The most common method is tag discovery:
aws ec2 create-tags \
--resources SUBNET_ID SECURITY_GROUP_ID \
--tags Key=karpenter.sh/discovery,Value=learn-karpenterReplace SUBNET_ID and SECURITY_GROUP_ID with the values in your cluster. This is how Karpenter learns which subnets and security groups it's allowed to use — we'll study the selector details in episode 5.
Now define your first two resources. The EC2NodeClass first, because the NodePool points to it.
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
amiFamily: AL2
role: "KarpenterNodeRole-eks-learn-karpenter"
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: learn-karpenter
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: learn-karpenterApply both:
kubectl apply -f ec2nodeclass.yaml
kubectl apply -f nodepool.yaml
kubectl get nodepoolsNotice the YAML structure: the NodePool points to the EC2NodeClass via nodeClassRef, while the capacity-type requirement is set to on-demand. We'll dissect the full NodePool structure in episode 4.
Now prove that Karpenter works. Deploy a workload that doesn't fit on the system nodegroup, then observe:
kubectl create deployment nginx --image=nginx --replicas=3
kubectl get pods -o wide
kubectl get nodeclaimsIf everything is correct, Karpenter will create a NodeClaim, launch a new instance, and the nginx pods will be scheduled onto that node. The label karpenter.sh/nodepool=default will appear on the new node — check with kubectl get nodes -l karpenter.sh/nodepool=default -o wide.
Important
If the pods stay Pending and no NodeClaim appears, the first debug step: check the controller logs with kubectl logs -n karpenter deployment/karpenter and make sure the subnet or security group tag discovery is correct. These two are the most common causes of provisioning failures on AWS.
Episode 3 guided you from zero until Karpenter actually provisions nodes. You created an EKS cluster, built a minimal IAM role, installed the CRDs and controller via Helm, set up tag discovery, then applied your first NodePool and EC2NodeClass, which were immediately tested with an nginx deployment.
Key takeaways:
In episode 4 we break down NodePool and scheduling: the spec.template structure, requirements, disruption, weight, and how Karpenter matches pods with pools — including the karpenter.sh labels and annotations attached to nodes. See you in the next episode!