Lesson 16 of 18 · Playbooks, challenges & practice
Challenges on this stack
Real-world problems you'll meet running this EKS stack, each with the symptom, how to confirm it and how to fix it for good: a plan that wants to replace the cluster, console drift, Karpenter not launching nodes, pods stuck on EBS volumes in another AZ, Ingresses without an address, surprise NAT costs and more.
How to use this lesson
Each challenge: symptom → confirm → fix → prevent. Many link to deeper lessons. (Generic incident method: Incident Handling, lessons 01–02.)
A field guide for a hiking trail: "if you see this sign, here's what it means and which path to take". You won't meet every problem on every hike, but you'll recognise them when you do.
1. The plan wants to replace the cluster
- Symptom:
module.eks.aws_eks_cluster.this[0] must be replaced. - Confirm: find the line marked
# forces replacementin the plan (name, some network settings, encryption config changes). - Fix: revert the change, or plan a real migration (new cluster, move workloads).
- Prevent:
lifecycle { prevent_destroy = true }on the cluster; CI fails plans containing replacements of critical resources.
2. Console drift
- Symptom: a plan you didn't expect shows changes (a security group rule, node group size).
- Confirm:
terraform plandiff; CloudTrail shows who changed it. - Fix: decide: revert (apply) or codify (update Terraform to match).
- Prevent: scheduled
plan -detailed-exitcodedrift checks (lesson 02); read-only console for humans in prod.
3. The chicken-and-egg bites on apply or destroy
- Symptom: kubernetes/helm provider errors connecting to a cluster that doesn't exist yet or is being destroyed.
- Fix: separate cluster and platform layers; destroy platform first (lesson 03).
4. Karpenter doesn't launch nodes
- Symptom: pods
Pending, no new nodes. - Confirm:
kubectl get nodeclaims,kubectl describe nodeclaim <n>, Karpenter logs (no compatible instance types, subnet/SG not found, insufficient capacity, IAM errors). - Fix: widen NodePool requirements (families, sizes, zones, capacity types); fix
EC2NodeClasssubnet/security-group selector tags; check the node IAM role/instance profile; check NodePool limits. - Prevent: alert on pods pending > N minutes; test NodePools in staging (lesson 06).
5. Pods stuck on EBS volumes in another AZ
- Symptom:
volume node affinity conflict, podPendingafter a node replacement. - Confirm:
kubectl get pv -o wideshows the volume's zone; no schedulable node in that zone. - Fix: make capacity available in that AZ (NodePool zones), or restore to a new volume in another AZ from a snapshot.
- Prevent: StorageClasses with
WaitForFirstConsumer; capacity in every AZ; EFS for data that must move between AZs (lesson 07).
6. Ingress has no ADDRESS
- Symptom:
kubectl get ingressshows no address; no ALB appears. - Confirm: LB controller logs: missing subnet tags (
kubernetes.io/role/elb), IAM permission errors, wrongingressClassName, invalid annotations. - Fix: tag subnets in the network layer; fix the controller's IAM (Pod Identity/IRSA); correct the Ingress.
- Prevent: smoke test in the creation playbook (lesson 13).
7. Pods can't get IP addresses
Subnet or ENI exhaustion with the VPC CNI. See Incident Handling, lesson 17 and lesson 04 here (prefix delegation, secondary CIDR).
8. Upgrade blocked or broken
- Symptom: add-on update fails; workloads break after the upgrade (removed APIs).
- Fix/prevent: pre-checks with
pluto, compatibility matrices for add-ons and controllers, staging first (lessons 09 and 14).
9. Surprise NAT gateway bill
- Symptom: high NAT data-processing charges.
- Confirm: VPC flow logs / Cost Explorer by usage type; image pulls and S3 traffic through NAT.
- Fix: VPC endpoints for ECR (api + dkr), S3 (gateway endpoint), STS, CloudWatch; keep traffic in-AZ where possible.
10. Locked out of the cluster
Access entries or aws-auth mistakes. See Incident Handling, lesson 18 and the recovery playbook (lesson 17).
Try it: provoke three challenges (sandbox)
- Change a replacement-forcing attribute in a test cluster config and read the plan (don't apply).
- Create a NodePool that allows only an instance type unavailable in your region's AZs; read the NodeClaim events.
- Remove the
kubernetes.io/role/elbtag from public subnets, create an Ingress, and read the controller logs; restore the tag.
Command summary
terraform plan | grep -E 'must be replaced|forces replacement'
terraform plan -detailed-exitcode
kubectl get nodeclaims ; kubectl describe nodeclaim <n> ; kubectl -n kube-system logs deploy/karpenter
kubectl get pv -o wide ; kubectl describe pvc <p>
kubectl -n kube-system logs deploy/aws-load-balancer-controller | tail
aws ec2 describe-subnets --filters Name=tag:kubernetes.io/role/elb,Values=1
Recap
- Catch replacements and drift in plans; protect critical resources with prevent_destroy.
- Karpenter: read NodeClaims and logs; fix requirements and selector tags.
- EBS is AZ-bound: WaitForFirstConsumer and capacity per AZ.
- No ALB: subnet tags, controller IAM, IngressClass. Cut NAT costs with VPC endpoints.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.