AWS EKS Deep Dive: Managed Kubernetes on AWS
Amazon EKS is AWS’s managed Kubernetes offering. AWS runs the control plane — multi-AZ etcd, API server, controller manager, scheduler — on infrastructure you never touch. You supply the nodes (or don’t, with Fargate). Simple premise; genuinely complex implementation once you hit production. This post covers everything that matters, including the decisions that come back to bite you at 2 a.m.
Current state as of May 2026: EKS supports Kubernetes 1.33, 1.34, and 1.35 on standard support. Versions 1.30–1.32 are on extended support (additional cost). The default version for new clusters is 1.35. Karpenter v1.x (GA since August 2024, currently at ~v1.12) is stable. EKS Pod Identity reached GA in late 2023. EKS access entries reached GA in early 2024. AL2 AMIs were end-of-life November 2025 — if you’re still on AL2 nodes, that’s your first action item.
Table of Contents
- Control Plane Architecture
- Worker Node Models: The Big Decision
- Networking Deep Dive — VPC CNI and Everything That Flows From It
- IRSA: IAM Roles for Service Accounts
- EKS Pod Identity (the newer way)
- EKS Add-ons Lifecycle
- Managed Node Groups in Depth
- Karpenter on EKS
- Fargate Profiles
- Cluster Upgrades with Zero Downtime
- EKS Anywhere
- Cluster Access Management: aws-auth vs Access Entries
- Cost Optimization Patterns
- Observability
1. Control Plane Architecture
AWS manages the control plane entirely. When you create a cluster, AWS provisions at minimum two API server instances and three etcd nodes spread across three Availability Zones in the region. You pay a flat $0.10/hour per cluster regardless of size. The cluster endpoint is a Network Load Balancer that AWS manages; you can make it public, private, or both.
The control plane runs in an AWS-owned VPC. It communicates with your worker nodes via cross-VPC ENIs that AWS injects into your VPC — these are the “cluster security group” ENIs you’ll see if you go looking. The cross-account ENI model is why your nodes need the cluster security group attached (or at least port 443 and 10250 open from the control plane CIDR).
Control plane logging — disabled by default, must be explicitly enabled. Five log types: api, audit, authenticator, controllerManager, scheduler. They land in CloudWatch Logs under /aws/eks/<cluster-name>/cluster. The audit log is what you need for security investigations. The authenticator log shows every IAM auth attempt. Enable them all in production; the cost is negligible, the regret for not having them is not.
|
|
Terraform:
|
|
gotcha: The subnet_ids in vpc_config are used by EKS to place cross-account ENIs, not to place your nodes. Node placement is controlled by the subnets in your node groups. Cluster subnets must have at least 5 available IPs at upgrade time — a constraint that has surprised many people running tight subnets.
2. Worker Node Models: The Big Decision
| Model | You manage | AWS manages | Best for |
|---|---|---|---|
| Managed Node Groups | Node lifecycle, AMI selection, scaling | Node provisioning, rolling updates, drain | General-purpose EC2 workloads |
| Self-managed nodes | Everything (ASG, AMI, drain logic) | Nothing at the node level | Custom OS, custom AMI, niche requirements |
| AWS Fargate | Nothing (serverless) | Nodes, patching, scaling | Batch jobs, bursty workloads, isolation |
| Karpenter | NodePool/EC2NodeClass config | Automatic node provisioning, bin-packing | Heterogeneous fleets, cost optimization |
| EKS Auto Mode | Almost nothing | Compute + networking + storage | Greenfield clusters, maximum managed |
Managed node groups are the right default for most teams. They handle the rolling update lifecycle (cordon, drain, replace) and use Launch Templates under the hood. The main constraint is that all nodes in a group use the same instance type family and size (though you can use a list).
Self-managed nodes are rarely the answer in 2026 unless you need something genuinely custom: specific kernel patches, custom CNI, non-EKS-optimized AMI, or GPU setups with unusual requirements. The operational burden is real.
Fargate eliminates node management entirely but comes with hard constraints (see section 9). Use it for workloads where the constraints don’t apply.
Karpenter is the right answer when you need heterogeneous fleets, aggressive cost optimization with Spot, or sub-minute node provisioning. It replaces the Cluster Autoscaler and does bin-packing at decision time rather than relying on static node groups.
The practical production pattern for most clusters: Managed node groups for your baseline system workloads (coredns, kube-proxy, monitoring agents) + Karpenter for application workloads.
3. Networking Deep Dive
VPC CNI: Secondary IP Mode vs Prefix Delegation
The default AWS VPC CNI (aws-vpc-cni) assigns real VPC IP addresses to every pod. Each pod gets an IP from your VPC subnet — no overlay network, no NAT for inter-pod traffic. This is elegant for observability and security group enforcement, but it burns IPs fast.
Secondary IP mode (default): Each node pre-warms a pool of secondary IPs from ENI slots. The number of IPs per node is bounded by (max ENIs per instance type) × (IPs per ENI - 1). On an m5.xlarge (3 ENIs × 15 IPs = 45 IPs minus 3 for primary = 42 max pods). This is the “ran out of IPs at 3 a.m.” failure mode.
Prefix delegation mode: Assigns /28 IPv4 prefixes (16 IPs) to ENI slots instead of individual IPs. An m5.xlarge with prefix delegation: 3 ENIs × 15 prefix slots × 16 IPs = 720 IPs available. This is a 17x improvement over secondary IP mode.
Enable prefix delegation:
|
|
Or via the EKS add-on:
|
|
Prefix delegation gotcha: The /28 blocks must be contiguous in your subnet. If your subnet is fragmented (common in long-running clusters), prefix allocation fails and pods stay Pending. Solution: Use dedicated subnets for node ENIs, or pre-warm with WARM_PREFIX_TARGET. IPv6 clusters require prefix delegation — it’s the only mode.
Subnet math: Before enabling prefix delegation, check for fragmentation:
|
|
Custom Networking (Different Subnets for Pods and Nodes)
If your nodes are in subnets with few IPs (common in older VPC designs) but you have large RFC 1918 secondary CIDR blocks, use custom networking to put pod IPs in the secondary CIDR subnets.
- Associate a secondary CIDR (
100.64.0.0/16is a popular choice) with your VPC - Create subnets from it per AZ
- Create ENIConfig objects pointing to those subnets
- Set
AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG=trueon the aws-node DaemonSet - Label nodes with
k8s.amazonaws.com/eniConfig=<az-name>or setENI_CONFIG_LABEL_DEF=topology.kubernetes.io/zone
gotcha: Custom networking and security groups for pods together use the security group from the security groups for pods config, not from ENIConfig. Only one wins.
Security Groups for Pods
Requires VPC CNI ENABLE_POD_ENI=true. Workable only on Nitro-based instances that have IsTrunkingCompatible: true (most m5, c5, r5, m6g, c6g, r6g — but NOT the t family). The VPC resource controller attaches a trunk ENI to the node and branches pod-specific ENIs off it.
The SecurityGroupPolicy CRD selects pods and applies a security group:
|
|
gotcha: POD_SECURITY_GROUP_ENFORCING_MODE defaults to strict, which breaks NodePort and LoadBalancer services with externalTrafficPolicy: Local. Switch to standard mode in production if you use those. Also: pods with security groups on NodeLocal DNSCache clusters require VPC CNI >= 1.11.0 and standard enforcing mode.
IPv6 EKS Clusters
IPv6 mode assigns each pod a unique IPv6 address from the VPC. No more IP exhaustion problems. Requires prefix delegation (it’s the only mode for IPv6). Each pod gets a /128 from a /80 prefix.
IPv6 constraints: AWS load balancers must support IPv6 (most do now). External clients need IPv6 connectivity or a dual-stack setup. Some third-party tools still don’t handle IPv6 well.
CoreDNS Tuning for High-Throughput Clusters
Default CoreDNS scales poorly beyond ~1000 pods because every DNS query goes to the CoreDNS ClusterIP, which is iptables NAT. At scale this produces conntrack table exhaustion and the infamous “5-second DNS timeout” (DNAT race condition + conntrack timeout).
NodeLocal DNSCache: A DaemonSet that runs a DNS cache on each node at 169.254.20.10. Pods query the local cache instead of the ClusterIP, bypassing iptables DNAT entirely.
|
|
CoreDNS Corefile tuning:
.:53 {
errors
health {
lameduck 30s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
prefer_udp
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
Scale CoreDNS replicas with the Cluster Proportional Autoscaler: one replica per 256 cores or 16 nodes, whichever comes first. If you’re throttling at the VPC DNS resolver (1024 packets/second per ENI limit), add replicas faster.
4. IRSA: IAM Roles for Service Accounts
IRSA lets pods assume IAM roles without storing credentials anywhere. Under the hood: the kube-apiserver projects a short-lived OIDC JWT token into the pod’s filesystem. The AWS SDK calls sts:AssumeRoleWithWebIdentity with that token. STS validates the token signature against the OIDC provider public key.
Step 1: Create the OIDC Provider
|
|
Or with eksctl (recommended):
|
|
Step 2: Trust Policy
|
|
StringEquals vs StringLike: Use StringEquals to lock down to a specific service account. Use StringLike with a wildcard (system:serviceaccount:my-namespace:*) to allow any service account in a namespace. Be careful with wildcards — a compromised namespace means a compromised role.
Step 3: Create the Role and Annotate the ServiceAccount
|
|
Or all at once with eksctl:
|
|
How the Token Projection Works
The mutating webhook (pod-identity-webhook) sees any pod using an annotated service account and injects two things:
- A projected volume with the OIDC token at
/var/run/secrets/eks.amazonaws.com/serviceaccount/token - Environment variables:
AWS_WEB_IDENTITY_TOKEN_FILEandAWS_ROLE_ARN
The AWS SDK picks these up automatically via the default credential chain. Token lifetime is 24 hours by default; the kubelet refreshes it before expiry.
Cross-account IRSA: The trust policy stays in account A (where the cluster is). Account B creates a role with a trust policy that allows Account A’s pod role to assume it via sts:AssumeRole. The pod assumes the Account A role first, then calls sts:AssumeRole to switch to Account B. Alternatively, modify the trust policy in Account B to directly federate the OIDC provider from Account A — both patterns work.
gotcha: The OIDC provider URL is cluster-specific. If you have 50 clusters, you have 50 OIDC providers, and every IAM role needs its trust policy updated per cluster. This is one of the main motivations for Pod Identity.
5. EKS Pod Identity
GA since late 2023. Pod Identity is a cleaner approach that doesn’t use per-cluster OIDC trust policies. The trust relationship uses a fixed service principal:
|
|
Same role, any EKS cluster. No OIDC provider needed. No per-cluster trust policy edits.
Setup
- Install the Pod Identity Agent add-on (DaemonSet on each node):
|
|
- Create a Pod Identity Association:
|
|
The Pod Identity Agent runs on each node at 169.254.170.23 (link-local). When a pod starts, the agent issues credentials to it directly. The load is per-node, not per-pod-per-SDK-call — a meaningful scalability improvement over IRSA at large cluster sizes.
IRSA vs Pod Identity: When to Use Which
| Concern | IRSA | Pod Identity |
|---|---|---|
| Cross-account access | Direct (OIDC federation) | Via IAM role chaining |
| Multi-cluster same role | Separate trust policy per cluster | Single trust policy for all clusters |
| Fargate support | Yes | No (EC2 only) |
| EKS Anywhere | Yes | No |
| Windows nodes | Not applicable | No |
| Outposts | Yes | No |
| Complexity of IAM role setup | Higher (OIDC per cluster) | Lower (one service principal) |
| Supports up to N associations | No hard limit | 5,000 per cluster |
Practical guidance: For new workloads on standard EKS clusters with EC2 nodes, use Pod Identity. For Fargate workloads, EKS Anywhere, cross-account with direct federation, or legacy compatibility, use IRSA. Both can coexist in the same cluster.
gotcha: Pod Identity associations are eventually consistent. Don’t create/update them in tight deployment automation loops — add a brief wait or check for the association to propagate.
6. EKS Add-ons Lifecycle
EKS manages a curated set of add-ons that it can install, update, and patch. Core add-ons installed automatically: vpc-cni, coredns, kube-proxy. Others you install on demand.
AWS-managed add-ons (key ones):
vpc-cni— pod networkingcoredns— cluster DNSkube-proxy— service networking (iptables/ipvs rules on each node)aws-ebs-csi-driver— EBS storage (requires IRSA or Pod Identity)aws-efs-csi-driver— EFS storagesnapshot-controller— VolumeSnapshot CRDseks-pod-identity-agent— Pod Identityamazon-cloudwatch-observability— Container Insightsadot— AWS Distro for OpenTelemetry
Version Management
Add-ons are NOT automatically upgraded when you upgrade the cluster. You must initiate add-on upgrades separately, and you can only upgrade one minor version at a time.
|
|
Conflict Resolution Modes
- OVERWRITE: EKS overwrites any in-cluster customizations with the add-on defaults. Use for initial creation or when you want to reset to defaults.
- PRESERVE: EKS updates the add-on version but preserves any fields you’ve modified in-cluster via Kubernetes API. Use for updates. This is now the recommended default for updates.
- NONE: The update fails if there are conflicts. Use if you want explicit control and don’t want silent overwrites.
Terraform pattern:
|
|
gotcha: If you created your cluster with eksctl without a config file, or with tools other than the AWS Console, you got self-managed add-ons (Helm charts or bare manifests), not EKS-managed add-ons. To get EKS management, you must adopt the add-on — set --resolve-conflicts OVERWRITE on the first create-addon call. After that, use PRESERVE.
gotcha: The eks:addon-cluster-admin ClusterRoleBinding must exist for add-ons to install. If someone deleted it for security hardening reasons (it does bind cluster-admin), add-ons stop working. Check before blaming the add-on itself.
7. Managed Node Groups in Depth
Managed node groups create an Auto Scaling Group in your account. EKS owns the lifecycle — provisioning, updating, terminating. You own the configuration.
AMI Families (as of 2026)
AL2 is gone (EOL November 2025). Current options:
- AL2023 (
AL2023_x86_64_STANDARD,AL2023_ARM_64_STANDARD,AL2023_x86_64_NEURON,AL2023_x86_64_NVIDIA) — recommended default - Bottlerocket (
BOTTLEROCKET_x86_64,BOTTLEROCKET_ARM_64,BOTTLEROCKET_x86_64_NVIDIA) — minimal OS, atomic updates, best security posture - Windows (
WINDOWS_CORE_2019_x86_64,WINDOWS_FULL_2022_x86_64, etc.)
AL2023 uses nodeadm for initialization — a YAML-based config system. If you use custom user data with a launch template, you must use MIME multi-part format for AL2023, TOML for Bottlerocket.
Terraform: Managed Node Group
|
|
Update Strategies
FORCE_UPDATE: Ignore PodDisruptionBudgets. Nodes are replaced regardless. Use only when you know what you’re doing.RESPECT_POD_DISRUPTION_BUDGET: Default. Honors PDBs. If a PDB blocks eviction, the update stalls. EKS will wait and retry. The update can appear stuck if your PDBs are too aggressive.
gotcha: If a PDB has minAvailable: 1 on a single-replica deployment, a node drain will block forever. Check your PDBs before upgrades. The standard debugging move is kubectl get pdb -A and kubectl describe pdb <name> to see which pods are blocking.
Custom User Data (AL2023 with nodeadm)
|
|
8. Karpenter on EKS
Karpenter v1 (GA August 2024, currently ~v1.12) replaces the Kubernetes Cluster Autoscaler for most EKS use cases. The Cluster Autoscaler scales existing node groups. Karpenter provisions new nodes from scratch based on pending pod requirements — it bins packs at decision time.
Key CRDs: NodePool and EC2NodeClass
EC2NodeClass — defines the AWS-specific node configuration:
|
|
NodePool — defines scheduling requirements and disruption behavior:
|
|
Spot Interruption Handling
Karpenter watches an SQS queue (populated by EventBridge rules) for EC2 Spot interruption warnings. On receiving a warning, it immediately cordons and drains the node, giving the 2-minute window maximum use. The EventBridge rules you need:
|
|
For Spot-to-Spot consolidation (replacing a larger spot instance with a smaller cheaper one), Karpenter requires a minimum of 15 diverse instance types in the NodePool requirements. Spot-to-Spot consolidation is off by default — the risk of consolidation triggering a cascade if spot capacity is tight is real.
Bin-Packing and Instance Selection
Karpenter selects the cheapest instance type that fits the pending pods’ combined resource requests. It evaluates all instance types matching the NodePool requirements, simulates scheduling all pending pods onto each candidate instance type, and picks the lowest-cost option. This is fundamentally different from Cluster Autoscaler which scales up the cheapest node group — Karpenter picks the cheapest individual instance type at decision time.
For cost optimization, the key levers are:
- Broad instance type diversity: The more instance types in the
requirements, the more likely Karpenter finds cheap capacity. - Multi-arch: Including
arm64(Graviton) opens ~40% cheaper instances for equivalent compute. - Spot first: Set
karpenter.sh/capacity-typeto includespotwith on-demand as fallback. - Consolidation:
WhenEmptyOrUnderutilizedcontinuously optimizes the fleet.
Drift Detection
Karpenter hashes the EC2NodeClass spec and NodePool requirements. When the hash changes (e.g., you update the AMI alias, change security groups, or upgrade the cluster and a new AMI becomes the latest for the family), existing nodes are marked as “drifted” and replaced rolling. This is how cluster upgrades propagate to Karpenter nodes automatically — upgrade the control plane, Karpenter detects AMI drift, replaces nodes.
gotcha: expireAfter: 720h (30 days) ensures nodes are replaced at least monthly even if no configuration changes. Without this, you can end up with very old nodes that are technically not drifted but running ancient AMIs.
gotcha: The karpenter.sh/do-not-disrupt: "true" annotation on a pod prevents voluntary disruption (consolidation, expiry, drift). It does NOT prevent interruption events or forced termination. Use it for genuinely long-running stateful work, but audit your pods periodically — it’s common for this annotation to get orphaned.
9. Fargate Profiles
Fargate runs pods on AWS-managed microVMs — one pod per VM, kernel isolation, no node to manage.
Profile Configuration
|
|
Pods matching ANY selector (namespace + optional label set) run on Fargate. Selectors are OR’d, not AND’d. A pod matches if it satisfies any one selector completely.
eksctl YAML:
|
|
What Does Not Work on Fargate
Hard limitations (as of 2026, confirmed from AWS docs):
| Feature | Available on Fargate |
|---|---|
| DaemonSets | No — use sidecars |
| HostNetwork | No |
| HostPort | No |
| Privileged containers | No |
| EBS volumes | No |
| EFS volumes | Yes (static provisioning) |
| GPU/Inferentia | No |
| Arm/Graviton | No |
| Windows containers | No |
| Fargate Spot | No (EKS Fargate Spot is not available) |
| Public subnets | No — private subnets with NAT only |
| Outposts / Local Zones | No |
| Custom CNI | No |
| IMDS access | No — use IRSA for credentials |
| Pod Identity | No — use IRSA |
The most commonly hit limitation in production: DaemonSets. If you’re using Datadog, Fluent Bit, or any other agent as a DaemonSet, it won’t run on Fargate nodes. Your options are sidecars in each pod (adds complexity, resource overhead) or run those workloads on EC2 nodes.
Cost
Fargate pricing is based on vCPU-seconds and GB-seconds of memory. Fargate rounds up to the next available configuration (minimum 0.25 vCPU, 0.5 GB). For a typical web service pod requesting 500m CPU / 512Mi, Fargate charges for 0.5 vCPU / 1 GB (rounds up). At US-East-1 rates, that’s roughly $0.020 vCPU-hour + $0.002 GB-hour ≈ $0.012/hour per pod. Compare to a t3.medium (2 vCPU, 4 GB) at ~$0.042/hour that can run 4 such pods ($0.0105/pod/hour). At small scale, Fargate is comparable. At large scale with proper bin-packing (Karpenter on Graviton Spot), EC2 is much cheaper.
When Fargate makes sense: Batch jobs with infrequent, unpredictable scheduling needs. Security-sensitive workloads requiring VM-level isolation. Teams that want zero node operations burden and accept the cost premium. Overflow burst capacity alongside a smaller EC2 baseline.
10. Cluster Upgrades with Zero Downtime
The rule that matters most: You cannot skip minor versions. 1.33 → 1.35 requires two upgrades: 1.33 → 1.34, then 1.34 → 1.35. Plan your upgrade cadence accordingly — falling multiple versions behind is an operational emergency.
Skew Policy
From Kubernetes 1.28+: the supported skew between the API server and kubelet extended from n-2 to n-3. Your nodes can be up to 3 minor versions behind the control plane. In practice, keep nodes within 1 version — the compatibility window exists for operational grace, not as a long-term config.
The Upgrade Sequence
1. Pre-upgrade checks
|
|
2. Upgrade the control plane
|
|
The API server briefly cycles during control plane upgrade. AWS does a rolling update across its managed instances, so there is a short period of reduced API server capacity. kubectl calls may get transient errors. This is expected and typically lasts 10–30 minutes.
3. Upgrade add-ons (immediately after control plane)
|
|
4. Upgrade node groups
|
|
5. Restart Fargate workloads
|
|
PodDisruptionBudgets During Upgrades
The node drain during upgrade respects PDBs. A well-configured PDB for a critical service:
|
|
topologySpreadConstraints to ensure pods land on multiple nodes/AZs during drain:
|
|
Blue/Green Cluster Upgrades
Alternative to in-place when: you need to skip multiple versions at once, you want a rollback option, or you’re migrating to a new cluster architecture simultaneously.
Downsides: Costs 2x during migration, API endpoint and OIDC issuer change (update kubeconfig everywhere, update IRSA trust policies for new cluster), stateful workloads require data migration. With GitOps (ArgoCD/Flux) this is much easier — just point the source at the new cluster endpoint.
eksctl Upgrade Workflow
|
|
11. EKS Anywhere
EKS Anywhere lets you run EKS-conformant clusters on your own infrastructure using the same APIs and tooling as cloud EKS. It is not the same as EKS — the control plane runs on your infrastructure (not AWS-managed), and many cloud-EKS features don’t exist.
Infrastructure Providers
- Bare metal (Tinkerbell-based provisioner)
- VMware vSphere
- Nutanix
- Apache CloudStack
- AWS Snow (for edge/disconnected environments)
- Docker (local development only)
Air-Gapped Operation
EKS Anywhere is designed for air-gapped environments. Pull all container images to a local registry mirror first:
|
|
For bare metal air-gapped clusters, you must set osImageURL and hookImagesURLPath in the cluster spec to point to your local image repository.
Curated Packages
EKS Anywhere Curated Packages are AWS-tested software packages available for enterprise subscribers: Prometheus, Grafana, Harbor (container registry), Emissary Ingress, MetalLB, Cert-Manager, ADOT, and others. Packages require an AWS Enterprise subscription and connectivity to AWS Package Controller (or mirror to local registry for air-gapped).
Key Differences from Cloud EKS
| Feature | Cloud EKS | EKS Anywhere |
|---|---|---|
| Control plane management | Fully managed by AWS | You manage (runs on your infra) |
| EKS add-ons | Curated, managed lifecycle | Available but self-managed |
| EKS Pod Identity | Yes | No |
| Fargate | Yes | No |
| Auto Scaling | AWS ASG / Karpenter | CAPI Machine Deployments |
| IAM integration | Native | Manual credential configuration |
| Pricing | Per-cluster-hour | Enterprise subscription (or free for basic) |
Use cases: On-premises environments with data residency requirements, factories/edge locations with intermittent connectivity, compliance environments where data cannot leave a physical location, air force-style disconnected classified deployments.
12. Cluster Access Management
The Old Way: aws-auth ConfigMap
The kube-system/aws-auth ConfigMap maps IAM principals to Kubernetes RBAC subjects. It was the only way to grant access for years. It is now deprecated.
|
|
The problem with aws-auth: It’s a ConfigMap. A typo breaks cluster access for everyone. The cluster creator has implicit cluster-admin — if they leave the company and their IAM user/role is deleted, you can potentially lose admin access to the cluster. No CloudTrail audit trail for changes. Messy to manage with IaC.
The New Way: EKS Access Entries (GA 2024)
Access entries are API objects — created via EKS API, tracked in CloudTrail, manageable with IaC. They work for clusters with platform version >= required (effectively all current clusters).
Step 1: Enable API authentication mode
|
|
Step 2: Create access entries
|
|
AWS managed access policies:
AmazonEKSClusterAdminPolicy— cluster-adminAmazonEKSAdminPolicy— admin (no cluster-level resources)AmazonEKSEditPolicy— edit (create/update/delete namespaced resources, no RBAC)AmazonEKSViewPolicy— view (read-only)AmazonEKSAdminViewPolicy— view everything including secrets
Terraform:
|
|
Migration from aws-auth
The safest migration path:
- Set
authenticationMode=API_AND_CONFIG_MAP— both work simultaneously - Create access entries for all principals currently in aws-auth
- Test access with each principal
- Set
authenticationMode=API— aws-auth is fully ignored - (Optional) Delete aws-auth to avoid confusion
gotcha: When switching to API mode, only the original cluster creator’s access entry is automatically created. All other entries from aws-auth must be manually recreated. If you delete aws-auth entries for managed node groups before creating the corresponding access entries, nodes stop functioning. Check that access entries for node roles exist before removing them from aws-auth.
13. Cost Optimization Patterns
Spot Instances with Karpenter
The most impactful cost lever. Spot can be 70–90% cheaper than on-demand for the same instance. The key to stable Spot usage:
- Diversify instance types broadly — 15+ instance types means AWS reclaiming one type doesn’t take you down
- Use capacity-weighted selection — Karpenter does this automatically
- Handle interruptions — Karpenter with SQS interrupt queue gives 2-minute warning drain
- Don’t run stateful workloads on Spot without checkpointing or replication
|
|
Use pod annotations to control which pool a workload lands on:
|
|
Graviton (arm64) Nodes
AWS Graviton instances (m7g, c7g, r7g, m6g, c6g, r6g) are typically 20–40% cheaper than equivalent x86 instances with equal or better performance for most workloads. Multi-arch images are necessary.
Build multi-arch images:
|
|
Enable Graviton in Karpenter NodePool:
|
|
gotcha: Some language runtimes and native extensions have subtle arm64 issues. Test thoroughly. Java, Python, Node.js, and Go are well-supported. C extensions and some Rust crates need recompilation.
Compute Savings Plans
Savings Plans commit to a consistent dollar amount of compute usage per hour (not specific instance types), giving 20–60% discount. For EKS:
- Compute Savings Plans (most flexible) — apply to any EC2 usage including EKS nodes, Lambda, Fargate
- EC2 Instance Savings Plans — locked to instance family and region, highest discount
Strategy: Use Compute Savings Plans for your on-demand baseline (what you’d run even at minimum load). Let Karpenter handle bursts with Spot. Layer multiple Savings Plans as your baseline grows.
KEDA for Event-Driven Scale-to-Zero
KEDA (Kubernetes Event-Driven Autoscaler) extends HPA with external event sources and enables scale-to-zero between events.
|
|
When combined with Karpenter, scale-to-zero workloads cause nodes to consolidate (WhenEmpty), so you pay nothing between event bursts.
VPA for Rightsizing
|
|
Run VPA in Off mode for two weeks, then check kubectl describe vpa my-app for the recommended values. Apply them to your deployment. This alone can cut compute costs 20–40% on over-provisioned services.
14. Observability
Container Insights
CloudWatch Container Insights collects cluster, node, pod, and container metrics. Two deployment models:
- DaemonSet (traditional): CloudWatch agent DaemonSet + Fluent Bit DaemonSet
- Managed add-on:
amazon-cloudwatch-observabilityadd-on (installs and manages both)
|
|
The add-on installs the CloudWatch agent and Fluent Bit. Requires an IAM role with CloudWatchAgentServerPolicy for the agent’s service account (use IRSA or Pod Identity).
EKS Control Plane Logging
Five log streams under /aws/eks/<cluster>/cluster in CloudWatch Logs:
kube-apiserver— API server requestskube-apiserver-audit— every API call with who made it and what they didauthenticator— IAM authentication attempts (success and failure)kube-controller-manager— reconciliation loops, garbage collectionkube-scheduler— scheduling decisions
For security investigations, the kube-apiserver-audit log is essential. Query it with CloudWatch Logs Insights:
# Find all secret reads in the last hour
fields @timestamp, @message
| filter @logStream like /kube-apiserver-audit/
| filter objectRef.resource == "secrets"
| filter verb == "get"
| sort @timestamp desc
| limit 100
Fluent Bit for Log Shipping
Fluent Bit is the standard log shipper for EKS. Installed automatically with the amazon-cloudwatch-observability add-on, or deployed independently.
Custom Fluent Bit configuration for shipping to multiple destinations:
|
|
ADOT (AWS Distro for OpenTelemetry)
ADOT is AWS’s distribution of OpenTelemetry Collector, available as an EKS add-on. Use it for:
- Shipping metrics to CloudWatch (Container Insights) or Amazon Managed Prometheus
- Shipping traces to AWS X-Ray
- Forwarding to third-party observability platforms (Datadog, Dynatrace) via OTLP
|
|
Configure via the AmazonCloudWatchAgent CRD (deployed by the add-on) or directly with OpenTelemetryCollector CRD.
gotcha: ADOT and Container Insights both use CloudWatch. Running both without coordination creates duplicate metrics and double cost. The amazon-cloudwatch-observability add-on includes ADOT-based collection internally. Don’t deploy the ADOT add-on separately if you’re using the CloudWatch Observability add-on unless you need traces or custom pipelines.
Common Production Gotchas Summary
- IP exhaustion: Enable prefix delegation early. Retrofitting it onto running clusters with fragmented subnets is painful.
- aws-auth typo: One malformed YAML in aws-auth breaks ALL cluster access. Migrate to access entries.
- Add-ons not auto-upgraded: After a cluster upgrade, add-ons stay at the old version. Explicitly upgrade them — especially VPC CNI and CoreDNS which have version compatibility requirements.
- Skipping minor versions: You can’t. Going from 1.30 to 1.35 requires 5 separate upgrades.
- PDB + single-replica deployments:
minAvailable: 1on a 1-replica deployment makes node drain impossible. UsemaxUnavailable: 1instead, or ensure >= 2 replicas. - Fargate + DaemonSets: Doesn’t work. Every monitoring, security scanning, or network tool that runs as a DaemonSet will not see Fargate pods.
- IRSA + OIDC per cluster: Every cluster has its own OIDC issuer URL in the trust policy. Use Pod Identity for new clusters to avoid this multiplication.
- Security groups for pods + t-family instances: t3, t3a, t4g instances don’t support trunk ENIs. Security groups for pods silently doesn’t work on them.
- Karpenter drift without expireAfter: Nodes won’t get replaced unless configuration changes. Set
expireAfter: 720hto force monthly rotation. - VPC subnet selection in cluster config: The subnets in the EKS cluster VPC config are for cross-account control-plane ENIs, not for your nodes. Don’t confuse them.
- Fargate Spot: It doesn’t exist for EKS. Don’t plan for it.
- Control plane logging disabled by default: Enable it before you need to debug. You cannot query logs that weren’t captured.
- Extended support cost: Clusters on Kubernetes versions past standard support incur additional hourly charges. Budget for this if you defer upgrades.
Sources:
- EKS Kubernetes Version Lifecycle
- Amazon EKS and EKS Distro now supports Kubernetes version 1.35
- Announcing Karpenter 1.0
- Karpenter Compatibility
- Karpenter Disruption
- Karpenter NodePools
- Karpenter EC2NodeClasses
- EKS Pod Identities
- Amazon EKS Pod Identity blog
- IRSA - Assign IAM roles to service accounts
- EKS Access Entries
- Migrating aws-auth to access entries
- Best Practices for Cluster Upgrades
- Security Groups for Pods
- Amazon VPC CNI Best Practices
- Prefix Mode for Linux
- EKS Add-ons
- Fargate on EKS
- EKS Anywhere Overview
- EKS Anywhere Air-gapped
- EKS Cluster Services Scaling
- Mixing Graviton with x86 on EKS
- EKS endoflife.date
- AL2023 upgrade for EKS
Comments