
Eks Best Practices
- 6 installs
- 39 repo stars
- Updated August 4, 2026
- aws-samples/sample-apex-skills
eks-best-practices is a Claude Code skill giving advisory guidance for Amazon EKS architecture and configuration decisions across compute, networking, security, cost, and upgrades.
About
This skill provides advisory guidance for Amazon EKS architecture and configuration decisions across compute, networking, security, reliability, cost, autoscaling, observability, and upgrades. A developer uses it when making an EKS planning or architectural judgment call, or when working with the terraform-aws-eks module. It consolidates the AWS EKS best-practices guides into decision tables and DO/DON'T lists.
- Advisory reference for EKS architecture: compute, networking, security, cost, autoscaling, upgrades
- Consolidates the AWS EKS Best Practices and HA/Resiliency guides plus terraform-aws-eks examples
- Includes decision tables for Fargate vs MNG vs Karpenter vs Auto Mode and VPC CNI modes
Eks Best Practices by the numbers
- 6 all-time installs (skills.sh)
- Ranked #870 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
eks-best-practices capabilities & compatibility
- Capabilities
- devops · security audit
- Works with
- aws · kubernetes · terraform
- Use cases
- devops · security audit
- Pricing
- Free
What eks-best-practices says it does
Advisory guidance for Amazon EKS architecture and configuration decisions
Default choice:** Karpenter — best balance of flexibility, cost, and automation
npx skills add https://github.com/aws-samples/sample-apex-skills --skill eks-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6 |
|---|---|
| repo stars | ★ 39 |
| Last updated | August 4, 2026 |
| Repository | aws-samples/sample-apex-skills ↗ |
What it does
Get advisory EKS architecture and configuration guidance for compute, networking, security, cost, and upgrades.
Who is it for?
EKS planning and architectural judgment calls like choosing compute or networking modes.
Skip if: Generating documents or code, auditing a live cluster, cluster discovery, or MCP setup.
When should I use this skill?
Any EKS planning or architectural judgment call, even when phrased casually.
What you get
Grounded architectural recommendations backed by AWS best-practice decision tables and DO/DON'T lists.
- EKS architecture recommendations
- Decision tables and DO/DON'T guidance
By the numbers
- Covers compute selection across 5 options (Fargate, MNG, Karpenter, Auto Mode, Self-Managed)
Files
EKS Best Practices
Comprehensive guidance for designing, deploying, and operating Amazon EKS clusters. Consolidates guidance from the AWS EKS Best Practices Guide, AWS EKS HA/Resiliency Guide, and terraform-aws-modules/terraform-aws-eks examples.
When to Use This Skill
Activate this skill when:
- Designing a new EKS cluster architecture
- Choosing between EKS compute options (Fargate, MNG, Karpenter, Auto Mode)
- Configuring EKS networking (VPC CNI, ingress, service mesh)
- Implementing EKS security (IAM, pod security, secrets)
- Planning cluster upgrades or migrations
- Reviewing EKS architecture decisions
- Working with terraform-aws-modules/terraform-aws-eks examples
- Optimizing EKS cost or scaling to large clusters
Don't use this skill for:
- Generic Kubernetes concepts (Claude knows these)
- Provider-specific API reference (link to AWS docs)
- Non-EKS container orchestration (ECS, Lambda)
- Step-by-step EKS upgrade execution — this skill covers upgrade strategy and architectural decisions, not the per-version procedures themselves.
EKS Architecture Decision Framework
When to Use EKS
| Requirement | EKS | ECS | Lambda |
|---|---|---|---|
| Kubernetes ecosystem | ✅ Native K8s | ❌ AWS-proprietary | ❌ |
| Portable across clouds | ✅ Standard K8s API | ❌ AWS-only | ❌ AWS-only |
| Long-running services | ✅ | ✅ | ⚠️ 15 min limit |
| Minimal ops overhead | Medium | Low | Lowest |
| GPU/ML workloads | ✅ Best support | Limited | ❌ |
| Complex networking | ✅ Full control | Medium | Limited |
| Team has K8s expertise | Required | Not required | Not required |
EKS Deployment Models
| Model | Description | Operational Overhead | Use When |
|---|---|---|---|
| EKS Standard | Full control over nodes, add-ons, networking | Medium-High | Need full customization |
| EKS Auto Mode | AWS manages nodes, add-ons, scaling | Low | Want minimal ops, standard workloads |
| EKS with Fargate | Serverless pods, per-pod billing | Low | Batch, low-density workloads |
| EKS on Outposts | Run EKS on-premises | High | Data residency, low-latency edge |
| EKS Anywhere | EKS on your own infrastructure | Highest | Air-gapped, custom hardware |
Shared Responsibility
| Component | AWS Manages | You Manage |
|---|---|---|
| Control plane | API server, etcd, HA, patching | RBAC, admission control, audit logging |
| Data plane (MNG) | AMI updates, node health | Instance type, scaling, pod scheduling |
| Data plane (Fargate) | Everything | Pod spec, resource requests |
| Data plane (Auto Mode) | Node lifecycle, OS patching | Workload definitions |
| Networking | ENI attachment, VPC CNI releases | Subnet design, IP planning, ingress |
| Security | Control plane auth | IAM, pod security, secrets, network policies |
Compute Selection Matrix
Decision Table
| Factor | Fargate | MNG | Karpenter | Auto Mode | Self-Managed |
|---|---|---|---|---|---|
| Best for | Batch, small scale | Stable, predictable | Dynamic, varied | Minimal ops | Custom AMI/kernel |
| Scaling | Per-pod | ASG-based | Fast, flexible | AWS-managed | Manual ASG |
| Spot support | ❌ | ✅ | ✅ Native | ✅ | ✅ |
| GPU support | ❌ | ✅ | ✅ | ✅ | ✅ |
| DaemonSets | ❌ | ✅ | ✅ | ✅ | ✅ |
| Cost model | Per vCPU/GB/hr | Per EC2 instance | Per EC2 instance | Per EC2 instance | Per EC2 instance |
| Max pods/node | 1 | ENI-based | ENI-based | AWS-managed | ENI-based |
| Node SSH | ❌ | ✅ | ✅ | ❌ | ✅ |
| Operational | Lowest | Low | Low | Lowest | Highest |
Quick Decision Guide
- Default choice: Karpenter — best balance of flexibility, cost, and automation
- Zero ops priority: EKS Auto Mode — AWS manages nodes, add-ons, and scaling via managed Karpenter. Best for teams that want Kubernetes benefits without operational overhead around upgrades, autoscaling, load balancing, and storage
- Serverless/batch: Fargate — no nodes to manage, per-pod billing
- Predictable, stable: MNG — familiar ASG model, managed updates
- Custom requirements: Self-managed — full control, highest overhead
✅ DO:
- Use Karpenter as the default node autoscaler for new clusters
- Run system components (CoreDNS, Karpenter) on MNG or Fargate
- Use multiple instance types for availability and cost optimization
❌ DON'T:
- Use self-managed nodes without a specific technical requirement
- Run Fargate for GPU or DaemonSet-dependent workloads
- Mix Karpenter and Cluster Autoscaler on the same node groups
Networking Quick Reference
VPC CNI Mode Decision
| Mode | Use When | Pod Density |
|---|---|---|
| Secondary IP (default) | Most workloads, simple setup | Limited by ENI × IPs per ENI |
| Prefix Delegation | >30 pods/node, IP-constrained VPC | 4-16× more pods per node |
| Custom Networking | Pods need different CIDR than nodes | Same as underlying mode |
Ingress Pattern Selection
| Pattern | Best For | Key Feature |
|---|---|---|
| ALB (via LBC) | HTTP/HTTPS web apps | Native WAF, Cognito auth |
| NLB (via LBC) | TCP/UDP, gRPC, low latency | Static IPs, source IP preservation |
| Gateway API | Multi-team, new deployments | ✅ Recommended standard |
| VPC Lattice | Cross-VPC service-to-service | No sidecar, IAM auth |
IPv4 vs IPv6
| Factor | IPv4 | IPv6 |
|---|---|---|
| Default choice | ✅ Yes | When facing IP exhaustion |
| AWS service support | Full | Most (check specific services) |
| Complexity | Standard | Requires dual-stack VPC |
For detailed networking guidance, see: Networking — VPC CNI & IP | Networking — Ingress & DNS
Security Essentials
IAM Strategy
| Approach | Use When | Setup |
|---|---|---|
| Pod Identity | ✅ New workloads (EKS 1.24+) | EKS add-on + association |
| IRSA | Older clusters, Fargate | OIDC provider + trust policy |
Key rules:
- ✅ Use Pod Identity for new workloads — simpler setup, session tags, role chaining
- ✅ Use EKS access entries (API mode) over aws-auth ConfigMap
- ✅ Move VPC CNI permissions from node role to Pod Identity/IRSA
- ❌ Don't use wildcard conditions in IRSA trust policies
- ❌ Don't attach application permissions to node IAM roles
Pod Security Baseline
Apply Pod Security Admission (PSA) labels to all namespaces:
# Minimum: enforce baseline, warn on restricted
metadata:
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/warn: restrictedSecrets Management
| Approach | Complexity | Best For |
|---|---|---|
| External Secrets Operator | Medium | ✅ GitOps workflows |
| Secrets Store CSI | Medium | Mount secrets as volumes |
| KMS envelope encryption | Low | Encrypt etcd secrets |
Always enable KMS envelope encryption for Kubernetes secrets.
For detailed security guidance, see: Security Reference | Runtime & Network | Supply Chain & Compliance
Reliability Essentials
Pod Disruption Budgets
Create PDBs for every production workload with >1 replica:
| Workload | Recommended PDB |
|---|---|
| Stateless (3+ replicas) | minAvailable: "50%" |
| Stateful quorum (3) | maxUnavailable: 1 |
| Batch/job | maxUnavailable: "50%" |
| Singleton | No PDB (would block all disruptions) |
Health Probe Strategy
| Probe | Purpose | Key Rule |
|---|---|---|
| Startup | Wait for slow init | Use for apps >10s startup |
| Readiness | Traffic routing | ✅ Check dependencies here |
| Liveness | Detect deadlocks | ❌ Never check dependencies |
Critical rule: Liveness probes must NOT check external dependencies. If the database goes down and liveness checks the DB, ALL pods restart — causing cascading failure.
Graceful Shutdown Pattern
spec:
terminationGracePeriodSeconds: 60
containers:
- lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]Why `sleep 15`: Gives kube-proxy and load balancer time to remove the pod from traffic routing before SIGTERM.
Multi-AZ Distribution
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotScheduleFor detailed reliability guidance, see: Reliability & Resiliency — Core (see also reliability-advanced.md for DR, deployment strategies, and large-cluster guidance)
Cluster Upgrade Strategy
Upgrade Sequence (Strict Order)
1. Control Plane → 2. EKS Add-ons → 3. Data Plane → 4. Custom Add-onsPre-Upgrade Checklist
1. Check EKS Cluster Insights for upgrade readiness 2. Scan for deprecated APIs (Pluto, kube-no-trouble) 3. Verify add-on compatibility with target version 4. Test in non-prod environment first 5. Ensure PDBs are configured for graceful node drain 6. Back up cluster state (Velero or GitOps repo)
Upgrade Strategy Decision
| Factor | In-Place | Blue-Green |
|---|---|---|
| Risk | Low-Medium | Lowest |
| Cost | No extra | 2× during migration |
| Rollback | ❌ No CP rollback | ✅ Switch back |
| Use when | ✅ Most upgrades | Critical workloads |
Data Plane with Karpenter
Karpenter automatically replaces nodes via drift detection after control plane upgrade. Control the speed with disruption.budgets:
disruption:
budgets:
- nodes: "10%" # Max 10% of nodes replaced at a timeFor detailed upgrade guidance, see: Cluster Upgrades Reference
Autoscaling Quick Reference
Node Autoscaler Selection
| Karpenter | Cluster Autoscaler | Auto Mode | |
|---|---|---|---|
| Default choice | ✅ Yes | Legacy/Outposts | Minimal ops |
| Scale-up speed | ~30s | ~60-90s | AWS-managed |
| Consolidation | ✅ Built-in | ❌ | ✅ |
| Customization | High | Medium | Low |
Pod Autoscaler Selection
| Scaler | Trigger | Use Case |
|---|---|---|
| HPA | CPU, memory, custom | Stateless services |
| VPA | Historical usage | Right-sizing (recommendation mode) |
| KEDA | External events (SQS, Kafka) | Event-driven workloads |
For detailed autoscaling guidance, see: Autoscaling Reference | Karpenter Reference
Terraform Examples Quick Start
Based on terraform-aws-modules/terraform-aws-eks.
Example Selection
| Starting Point | Recommended Example |
|---|---|
| General production | karpenter (MNG for system + Karpenter for workloads) |
| Minimal ops | eks-auto-mode |
| Managed nodes | eks-managed-node-group (AL2023 or Bottlerocket) |
| Full node control | self-managed-node-group |
| Platform capabilities | eks-capabilities (ArgoCD, ACK, KRO) |
| Hybrid/edge | eks-hybrid-nodes |
Common Deployment Topologies
Private cluster with Karpenter:
VPC (3 AZs, terraform-aws-modules/vpc/aws)
├── Private subnets → EKS nodes (MNG for system, Karpenter for workloads)
├── Public subnets → ALB (internet-facing)
├── Intra subnets → EKS control plane ENIs
└── NAT Gateway → 1 per AZ for productionMulti-tenant platform:
EKS Cluster (terraform-aws-modules/eks/aws)
├── kube-system (platform: CoreDNS, kube-proxy, VPC CNI)
├── karpenter (Karpenter controller on MNG)
├── monitoring (shared: Prometheus, Grafana)
├── ingress (shared: AWS LBC)
├── team-a namespace (RBAC, NetworkPolicy, ResourceQuota)
├── team-b namespace (RBAC, NetworkPolicy, ResourceQuota)
└── team-c namespace (RBAC, NetworkPolicy, ResourceQuota)For detailed examples and terraform patterns, see: Terraform Examples Reference
Cost Optimization Quick Wins
| Action | Savings | Effort |
|---|---|---|
| Graviton (arm64) | 20-40% | Low |
| Spot for non-critical | 60-90% | Low |
| Karpenter consolidation | 20-30% | Low |
| VPA right-sizing | 15-30% | Medium |
| gp3 over gp2 | 20% on EBS | Low |
| VPC endpoints | Eliminate NAT costs | Low |
For detailed cost guidance, see: Cost Optimization Reference | For scalability guidance, see: Scalability Reference
Observability Quick Reference
| Pillar | AWS-Managed | Open Source |
|---|---|---|
| Metrics | Container Insights | AMP + Grafana |
| Logs | CloudWatch Logs | OpenSearch, Loki |
| Traces | X-Ray | ADOT + Jaeger/Tempo |
Essential: Enable EKS audit logging and GuardDuty EKS Runtime Monitoring for security visibility.
For detailed observability guidance, see: Observability Reference
EKS Capabilities
EKS Capabilities are AWS-managed features installed and updated as part of the EKS platform. They run in AWS-owned infrastructure separate from your clusters, with AWS handling scaling, patching, and upgrading.
| Capability | What It Does | When to Use Managed | When to Self-Manage |
|---|---|---|---|
| ArgoCD | GitOps continuous delivery | Multi-account hub-and-spoke, IAM IDC integration, minimal ops | Custom plugins, air-gapped, existing ArgoCD investment |
| ACK | Manage AWS resources via K8s CRDs (S3, RDS, IAM, etc.) | Standard AWS resource management | Specific controller version pinning, custom config |
| KRO | Platform abstractions via ResourceGroupDefinitions | Golden path templates, multi-resource compositions | Early adoption risk concerns, custom reconciliation logic |
Combined pattern: ArgoCD deploys ACK resources + KRO compositions via GitOps, providing a single workflow for both infrastructure and applications.
For detailed ArgoCD patterns, see: ArgoCD Patterns Reference
Sources:
- EKS Capabilities Documentation
- AWS Blog — Deep dive: Simplifying resource orchestration with Amazon EKS Capabilities
Detailed References
This skill uses progressive disclosure — essential guidance is in this main file, detailed reference material is loaded on demand:
- [Security](references/security.md) — IAM, Cluster Access Manager, Pod Identity, IRSA, pod security standards, multi-tenancy, secrets management, data encryption
- [Security — Runtime & Network](references/security-runtime-network.md) — Runtime threat detection (GuardDuty, seccomp, AppArmor, Falco), network policies, SG for pods, encryption in transit, detective controls
- [Security — Supply Chain & Compliance](references/security-supply-chain.md) — Image security (SBOMs, attestations, ECR hardening), infrastructure hardening (Bottlerocket, CIS benchmarks), regulatory compliance, incident response
- [Networking](references/networking.md) — VPC CNI modes (secondary IP, prefix delegation, custom networking), subnet/CIDR planning, IPv4 vs IPv6, Security Groups for Pods, IP address management
- [Networking — Ingress & DNS](references/networking-ingress-dns.md) — Ingress patterns (ALB, NLB, Gateway API), AWS Load Balancer Controller, service mesh, DNS/CoreDNS tuning, private cluster connectivity
- [Reliability & Resiliency — Core](references/reliability-core.md) — HA patterns, PDBs, health probes, load balancer health checks, lifecycle hooks, topology spread, resource management
- [Reliability & Resiliency — Advanced](references/reliability-advanced.md) — disaster recovery, zonal shift, deployment strategies, large cluster guidance, chaos engineering, admission-controller topology enforcement
- [Autoscaling](references/autoscaling.md) — Autoscaler selection, Cluster Autoscaler (IAM, Spot, overprovisioning, parameter tuning), HPA, VPA, KEDA, CoreDNS autoscaling
- [Karpenter](references/karpenter.md) — Operational best practices, NodePools, EC2NodeClass, Spot/interruption handling, consolidation, multiple NodePool strategy, cost controls, resource management, private clusters, CoreDNS with Karpenter
- [Cluster Upgrades](references/cluster-upgrades.md) — In-place and blue-green upgrades, pre-upgrade validation, add-on management, API deprecation detection, version skew policy, Bottlerocket updates, rollback procedures
- [Cost Optimization](references/cost-optimization.md) — CFM framework, compute/networking/storage cost strategies, observability cost management, Spot, Graviton, tagging, Kubecost
- [Scalability](references/scalability.md) — Scaling theory (churn rate, QPS), control plane (APF, monitoring), data plane (node sizing, diversity), cluster services (CoreDNS, Metrics Server), workload patterns, IPVS, large-cluster guidance
- [Observability](references/observability.md) — Observability strategy, CloudWatch Container Insights & Application Signals, Prometheus/Grafana, control plane monitoring, network performance monitoring, logging architecture, distributed tracing, GPU/AI-ML observability, detective controls, alerting patterns
- [Terraform Examples](references/terraform-examples.md) — terraform-aws-modules/terraform-aws-eks examples, submodules, add-on management, Provisioned Control Plane, EFA, VPC patterns, deployment topologies
- [ArgoCD Patterns](references/argocd-patterns.md) — ArgoCD architecture, App of Apps, ApplicationSets, GitOps Bridge, multi-cluster patterns (hub-and-spoke, decentralized, hybrid), EKS ArgoCD Capability (managed vs self-managed, migration), ACK/KRO integration, multi-tenant RBAC
- [Container Registry](references/container-registry.md) — ECR architecture, operating models, image promotion, vulnerability scanning, base image curation, lifecycle policies, pull-through cache, repository creation templates, managed signing (AWS Signer), archival storage class, registry configuration
- [EKS Auto Mode](references/eks-auto-mode.md) — Auto Mode architecture, managed NodePools/NodeClasses, migration from standard EKS, comparison with self-managed Karpenter, limitations and FAQ
How to use: When you need detailed information on a topic, reference the appropriate guide. Claude will load it on demand.
Sources
ArgoCD Patterns for EKS
Part of: eks-best-practices
Purpose: ArgoCD architecture patterns, deployment strategies, and multi-tenant RBAC for Amazon EKS
---
Table of Contents
1. ArgoCD Architecture on EKS 2. App of Apps Pattern 3. ApplicationSets 4. GitOps Bridge Pattern 5. Multi-Tenant RBAC 6. EKS ArgoCD Capability (Managed) 7. ACK and KRO Integration
---
ArgoCD Architecture on EKS
ArgoCD is a declarative GitOps continuous delivery tool for Kubernetes. It continuously monitors source repositories and reconciles the desired state with the live state in the cluster. ArgoCD supports multiple source types: Git repositories, Helm registries (HTTP and OCI), and OCI images — giving flexibility for different security and compliance requirements.
Core Components
| Component | Role | Resource Profile |
|---|---|---|
| API Server | UI, CLI, API access, authentication | Low CPU, moderate memory |
| Repo Server | Clones Git repos, renders manifests (Helm, Kustomize) | CPU-intensive during sync |
| Application Controller | Reconciliation loop, compares desired vs live state | Memory-intensive (caches cluster state) |
| Redis | Caching layer for controller and API server | Low resource, critical for performance |
| Dex / SSO | Authentication (OIDC, SAML, LDAP) | Low resource |
| Notifications Controller | Sends alerts on sync status changes | Low resource |
Self-Managed vs EKS Managed ArgoCD
| Factor | Self-Managed (Helm) | EKS ArgoCD Capability |
|---|---|---|
| Installation | Helm chart, you manage upgrades | AWS-managed, automatic upgrades |
| Control plane | Runs in your cluster | Runs in AWS-owned infrastructure |
| Customization | Full (plugins, custom tooling, RBAC) | Limited to supported configuration |
| Multi-cluster | Manual setup (cluster secrets) | Built-in hub-and-spoke |
| IAM integration | Manual OIDC/SAML setup | Native IAM Identity Center |
| Cost | Cluster compute only | EKS Capability pricing |
| Best for | Custom plugins, air-gapped, existing investment | Minimal ops, multi-account, new deployments |
---
App of Apps Pattern
The App of Apps pattern uses a single "parent" ArgoCD Application that contains manifests defining other ArgoCD Applications. This enables declarative bootstrapping of an entire platform from a single entry point.
Structure
| Level | What It Contains | Example |
|---|---|---|
| Root App | Points to a directory of Application manifests | platform-root → apps/ directory |
| Platform Apps | Infrastructure add-ons (monitoring, ingress, cert-manager) | apps/monitoring.yaml, apps/ingress.yaml |
| Tenant Apps | Team workloads | apps/team-a.yaml, apps/team-b.yaml |
When to Use
| Scenario | App of Apps | ApplicationSets |
|---|---|---|
| Cluster bootstrapping | Yes — single entry point for all platform components | Possible but more complex |
| Static set of apps | Good fit — each app is an explicit manifest | Overkill |
| Dynamic app generation | Poor fit — must manually create each manifest | Better — template-based |
| Multi-cluster | One root app per cluster | One ApplicationSet generates across clusters |
Benefits and Limitations
| Benefits | Limitations |
|---|---|
| Single entry point for cluster bootstrap | Each new app requires a new manifest file |
| Explicit — every app is visible in Git | Doesn't scale well to hundreds of apps |
| Easy to understand and debug | No templating — each manifest is hand-written |
| Works with any manifest format | Changes require Git commit per app |
---
ApplicationSets
ApplicationSets are a templating mechanism that generates multiple ArgoCD Applications from a single definition. They use generators to produce parameter sets, which are then applied to a template to create Applications.
Generators
| Generator | What It Does | Use Case |
|---|---|---|
| Git Directory | Creates an app per directory in a Git repo | Monorepo with one dir per service |
| Git File | Creates an app per config file in a Git repo | Config-driven app definitions |
| Cluster | Creates an app per registered cluster | Multi-cluster deployments |
| Matrix | Combines two generators (cross-product) | Per-cluster × per-environment |
| Merge | Combines generators with override logic | Base config + cluster-specific overrides |
| List | Static list of parameter sets | Small, known set of targets |
| Pull Request | Creates app per open PR | Preview environments |
Common Patterns
| Pattern | Generator | Description |
|---|---|---|
| Per-environment | Git Directory | environments/dev/, environments/staging/, environments/prod/ |
| Per-cluster | Cluster | Deploy same app to all registered clusters |
| Per-tenant | Git File | One config file per tenant defines their apps |
| Preview environments | Pull Request | Ephemeral environment per PR |
| Matrix (cluster × env) | Matrix | Deploy to every cluster in every environment |
ApplicationSets vs App of Apps
| Factor | ApplicationSets | App of Apps |
|---|---|---|
| Scaling | Handles hundreds of apps via templates | Manual — one manifest per app |
| Dynamic generation | Yes — new directory/file/cluster auto-generates | No — must commit new manifest |
| Complexity | Higher (generator logic, template syntax) | Lower (plain Application manifests) |
| Debugging | Harder (template rendering issues) | Easier (explicit manifests) |
| Recommendation | Use for dynamic, large-scale deployments | Use for static platform bootstrapping |
---
GitOps Bridge Pattern
The GitOps Bridge pattern separates infrastructure provisioning (Terraform) from application/add-on management (ArgoCD). Terraform creates the cluster and bootstraps ArgoCD; ArgoCD manages everything else.
How It Works
| Phase | Tool | What It Does |
|---|---|---|
| 1. Infrastructure | Terraform | Provisions VPC, EKS cluster, node groups, IAM roles |
| 2. Bootstrap | Terraform | Installs ArgoCD via Helm, creates root Application |
| 3. Add-ons | ArgoCD | Manages all cluster add-ons (monitoring, ingress, policy engine) |
| 4. Applications | ArgoCD | Manages all workload deployments |
Integration with terraform-aws-modules/eks
The terraform-aws-modules/eks module provisions the cluster. Terraform then installs ArgoCD and creates a bootstrap Application pointing to the GitOps repository. From that point, ArgoCD takes over management of add-ons and applications.
| Terraform Manages | ArgoCD Manages |
|---|---|
| VPC, subnets, NAT | Cluster add-ons (monitoring, ingress, cert-manager) |
| EKS cluster, node groups | Policy engine (Kyverno, Gatekeeper) |
| IAM roles (cluster, node, Pod Identity) | Application deployments |
| KMS keys, S3 buckets | Namespace configuration, RBAC |
| ArgoCD installation (bootstrap only) | ArgoCD self-management (after bootstrap) |
---
Multi-Tenant RBAC
ArgoCD Projects provide tenant isolation by restricting what each team can deploy, where they can deploy, and what cluster resources they can access.
AppProject Scoping
| Restriction | What It Controls | Example |
|---|---|---|
| Source repos | Which Git repos the project can read | https://github.com/org/team-a-* |
| Destinations | Which clusters and namespaces apps can deploy to | cluster: in-cluster, namespace: team-a-* |
| Cluster resources | Which cluster-scoped resources are allowed | Deny ClusterRole, Namespace creation |
| Namespaced resources | Which namespaced resources are allowed/denied | Allow Deployment, Service; deny ResourceQuota |
SSO Integration
| Provider | Integration Method | Notes |
|---|---|---|
| OIDC | Dex connector or built-in OIDC | Most common; works with Okta, Azure AD, Google |
| SAML | Dex connector | Enterprise SSO |
| LDAP | Dex connector | On-premises directory |
| IAM Identity Center | EKS ArgoCD Capability only | Native AWS SSO |
RBAC Model
| Role | Scope | Permissions |
|---|---|---|
| Platform admin | All projects | Full access — create/delete apps, manage projects |
| Tenant admin | Own project | Create/sync/delete apps within project boundaries |
| Tenant developer | Own project | Sync apps, view logs; cannot create or delete |
| Viewer | All or specific projects | Read-only — view app status, logs |
---
Multi-Cluster Architecture Patterns
When deploying ArgoCD (managed or self-managed) across multiple clusters, three patterns emerge:
Hub-and-Spoke (Centralized)
All capabilities run on a central management cluster that orchestrates workloads and infrastructure across spoke clusters.
| Component | Where It Runs | What It Does |
|---|---|---|
| ArgoCD | Management cluster | Deploys apps to all workload clusters |
| ACK | Management cluster | Provisions AWS resources (RDS, S3, IAM) for all clusters |
| KRO | Management cluster | Creates portable abstractions across all clusters |
Best for: Centralized platform teams, audit/compliance requirements, fleet management.
Decentralized
Each cluster runs its own capabilities independently.
| Component | Where It Runs | What It Does |
|---|---|---|
| ArgoCD | Each cluster | Manages local applications only |
| ACK | Each cluster | Provisions resources for local workloads |
| KRO | Each cluster | Local resource compositions |
Best for: Autonomous teams, independent clusters, minimal cross-cluster dependencies.
Hybrid (Hub + Local ACK)
Combines centralized GitOps delivery with local resource management based on scope.
| Component | Hub Cluster | Spoke Clusters |
|---|---|---|
| ArgoCD | Deploys to all clusters | N/A (managed from hub) |
| ACK | Admin-scoped resources (production DBs, IAM, VPCs) | Workload-scoped resources (S3 buckets, SQS queues) |
| KRO | Reusable platform abstractions | Local building block patterns |
Best for: Platform teams managing critical infrastructure centrally while enabling self-service for application teams.
Choosing a Pattern
| Factor | Hub-and-Spoke | Decentralized | Hybrid |
|---|---|---|---|
| Org structure | Centralized platform team | Autonomous teams | Platform team + app teams |
| Compliance | Easiest to audit | Hardest to audit | Balanced |
| Operational complexity | Fewer instances, possible bottleneck | More instances to manage | Moderate |
| Self-service | Via KRO abstractions from hub | Full autonomy | Admin from hub, workload locally |
You can start with one pattern and evolve — capabilities are independent and can be deployed differently across clusters.
---
EKS ArgoCD Capability (Managed)
The EKS ArgoCD Capability is an AWS-managed ArgoCD service that runs in AWS-owned infrastructure, separate from your cluster. AWS handles scaling, patching, and upgrading. It does not consume worker node resources.
| Feature | Detail |
|---|---|
| Control plane | Runs in AWS infrastructure (not in your cluster, no node resource consumption) |
| Multi-cluster | Built-in hub-and-spoke — manage multiple EKS clusters from one instance |
| Authentication | Native IAM Identity Center integration (3 roles: admin, editor, viewer) |
| Upgrades | AWS-managed, automatic |
| Sources | Git (HTTPS/SSH), Helm registries (HTTP/OCI), OCI images, CodeCommit, CodeConnections (GitHub, GitLab, Bitbucket) |
| Secrets | Native Secrets Manager integration |
| Registry | Native ECR integration (OCI Helm charts) |
| Private clusters | Transparent access — no VPC peering or special networking needed |
| Remote clusters | Uses EKS access entries — no IRSA or cross-account role assumptions needed |
| Pricing | Hourly per capability + hourly per managed K8s resource |
Managed Capability Limitations
Features not available in the managed capability (use self-managed if you need these):
| Unsupported Feature | Workaround |
|---|---|
| Config Management Plugins (CMPs) | Pre-render manifests in CI pipeline |
| Custom Lua health checks | Built-in checks cover standard resources |
| Notifications controller | Use EventBridge + CloudWatch for alerting |
| Custom SSO providers | Only IAM Identity Center (supports third-party federation through Identity Center) |
| UI extensions / custom banners | N/A |
| Direct argocd-cm ConfigMap access | Configure via capability API |
| Custom sync timeout | Fixed at 120 seconds |
Key Operational Differences
- Single namespace: All ArgoCD CRs (Application, ApplicationSet, AppProject) must be created in one namespace (default:
argocd). Workloads deploy to any namespace in any target cluster. - Only EKS targets: Deployment targets must be EKS clusters identified by ARN (not arbitrary Kubernetes API server URLs).
- Local cluster not auto-registered: You must explicitly register the local cluster using its ARN to deploy to it.
- CLI differences: Use
argocd app sync namespace/appname(namespace prefix required).argocd adminandargocd loginare not supported — use account or project tokens. - Namespace isolation: Keep only ArgoCD-relevant secrets in the ArgoCD namespace — the capability has access to all secrets in its namespace.
When to Use EKS Managed vs Self-Managed
| Scenario | Recommendation |
|---|---|
| New deployment, minimal ops team | EKS Managed |
| Multi-account, hub-and-spoke | EKS Managed |
| Need IAM Identity Center SSO | EKS Managed |
| Private clusters with no extra networking | EKS Managed |
| Custom ArgoCD plugins (CMPs) | Self-managed |
| Air-gapped environment | Self-managed |
| Existing ArgoCD investment | Self-managed (unless migrating) |
| Need specific ArgoCD version | Self-managed |
| Need Notifications controller | Self-managed |
Migrating from Self-Managed to EKS Managed
1. Review current config for unsupported features (CMPs, custom Lua, Notifications, UI extensions) 2. Scale self-managed ArgoCD controllers to zero replicas 3. Create ArgoCD capability resource on your cluster 4. Export existing Applications, ApplicationSets, and AppProjects 5. Migrate repository credentials and cluster secrets 6. Update destination.server fields to use cluster names or EKS cluster ARNs 7. Apply manifests to the managed instance 8. Verify applications sync correctly 9. Decommission self-managed installation
Existing Application/ApplicationSet manifests work with minimal modification — the managed capability uses the same Kubernetes APIs and CRDs.
---
ACK and KRO Integration
AWS Controllers for Kubernetes (ACK)
ACK lets you manage AWS resources using Kubernetes custom resources. Instead of using Terraform or CloudFormation for AWS resources, you define them as Kubernetes manifests that ArgoCD can manage via GitOps. ACK translates Kubernetes CR specs into AWS API calls and continuously reconciles to maintain desired state (detecting and correcting drift).
| ACK Controller | AWS Resources Managed |
|---|---|
| S3 | Buckets, bucket policies |
| RDS | DB instances, DB clusters, parameter groups |
| IAM | Roles, policies |
| EC2 | Security groups, VPC resources |
| SQS | Queues |
| SNS | Topics, subscriptions |
| DynamoDB | Tables |
| ElastiCache | Clusters, replication groups |
| Lambda | Functions, event source mappings |
| EKS | Clusters, node groups |
| Feature | Description |
|---|---|
| Multi-account/multi-region | Manage AWS resources across multiple AWS accounts and regions from a single cluster |
| Resource adoption | Bring existing AWS resources under ACK management without recreation |
| Read-only resources | Observe AWS resources without modification access |
| Retention annotations | Optionally retain AWS resources when Kubernetes CRs are deleted |
Kube Resource Orchestrator (KRO)
KRO provides platform abstractions via ResourceGraphDefinitions (RGDs). Platform teams define golden path templates that combine multiple resources (Kubernetes + AWS via ACK) into a single custom resource that tenant teams consume. KRO automatically determines interdependencies and resource ordering, and uses Common Expression Language (CEL) for injecting values between resources and conditional logic.
| Concept | What It Does | Example |
|---|---|---|
| ResourceGraphDefinition | Defines a custom API combining multiple resources | "WebApp" = Deployment + Service + Ingress + RDS (via ACK) |
| Instance | A tenant creates an instance of the template | kind: WebApp, name: team-a-api |
| Reconciliation | KRO creates and manages all child resources | Creates Deployment, Service, Ingress, RDS instance |
Combined Pattern: ArgoCD + ACK + KRO
| Layer | Tool | What It Manages |
|---|---|---|
| GitOps delivery | ArgoCD | Syncs all manifests from Git to cluster |
| AWS resources | ACK | Creates/updates AWS resources (RDS, S3, SQS) as K8s CRDs |
| Platform abstractions | KRO | Combines K8s + ACK resources into golden path templates |
| Tenant self-service | KRO instances | Tenants create a single CR, get a full stack |
---
Sources:
EKS Autoscaling Best Practices
Part of: eks-best-practices
Purpose: Autoscaler selection, Cluster Autoscaler, HPA, VPA, KEDA, and CoreDNS autoscaling for Amazon EKS
---
Table of Contents
1. Autoscaler Selection 2. Cluster Autoscaler 3. EKS Auto Mode 4. Horizontal Pod Autoscaler 5. Vertical Pod Autoscaler 6. KEDA Event-Driven Autoscaling 7. CoreDNS Autoscaling
---
Autoscaler Selection
Node Autoscaler Decision Matrix
| Factor | Karpenter | Cluster Autoscaler (CAS) | EKS Auto Mode |
|---|---|---|---|
| Instance selection | Flexible -- picks optimal from many types | Fixed to node group instance types | AWS-managed selection |
| Scale-up speed | Fast (~30s to provision) | Moderate (~60-90s) | Fast (AWS-managed) |
| Consolidation | Built-in, configurable | No native consolidation | AWS-managed |
| Spot support | Native Spot handling | Via mixed instance policy | AWS-managed |
| Operational overhead | Low -- CRD-based config | Medium -- ASG management | Lowest -- fully managed |
| Customization | High | Medium | Low |
| Maturity | GA (v1.0+) | Very mature | Newer -- evaluate for your use case |
| Recommendation | Default choice | When Karpenter is not an option | When minimal ops is top priority |
For detailed Karpenter guidance, see: Karpenter Reference
Pod Autoscaler Decision Matrix
| Scaler | Trigger | Use When |
|---|---|---|
| HPA | CPU, memory, custom metrics | Stateless workloads with predictable load patterns |
| VPA | Historical resource usage | Right-sizing, non-scaling workloads |
| KEDA | External events (SQS, Kafka, etc.) | Event-driven, queue-based workloads |
| HPA + KEDA | Combined metrics | Complex scaling with both resource and event triggers |
---
Cluster Autoscaler
When to Use CAS Over Karpenter
- EKS on Outposts (Karpenter not supported)
- Self-managed node groups with specific AMI requirements
- Clusters already running CAS with complex ASG configurations
- Organizations requiring node group-level operational boundaries
Operating the Cluster Autoscaler
CAS runs as a single-replica Deployment using leader election for HA. It is not horizontally scalable -- scale it vertically for large clusters.
Key requirements:
- Version must match cluster version -- cross-version compatibility is not tested or supported
- Enable Auto Discovery -- unless you have specific advanced use cases requiring manual ASG configuration
- Use EKS Managed Node Groups -- they provide automatic ASG discovery and graceful node termination
IAM Least Privilege
Scope CAS's IAM permissions to the cluster's own ASGs. This prevents a CAS instance in one cluster from modifying node groups in another cluster:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"autoscaling:SetDesiredCapacity",
"autoscaling:TerminateInstanceInAutoScalingGroup"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/k8s.io/cluster-autoscaler/enabled": "true",
"aws:ResourceTag/k8s.io/cluster-autoscaler/my-cluster": "owned"
}
}
},
{
"Effect": "Allow",
"Action": [
"autoscaling:DescribeAutoScalingGroups",
"autoscaling:DescribeAutoScalingInstances",
"autoscaling:DescribeLaunchConfigurations",
"autoscaling:DescribeScalingActivities",
"autoscaling:DescribeTags",
"ec2:DescribeImages",
"ec2:DescribeInstanceTypes",
"ec2:DescribeLaunchTemplateVersions",
"ec2:GetInstanceTypesFromInstanceRequirements",
"eks:DescribeNodegroup"
],
"Resource": "*"
}
]
}Node Group Configuration
Nodes within a node group must have identical scheduling properties (labels, taints, resources). For MixedInstancePolicies:
- Instance types must have the same shape for CPU, memory, and GPU
- The first instance type in the policy is used for scheduling simulation
- Larger subsequent types waste resources after scale-out; smaller ones cause scheduling failures
Design principles:
- Prefer fewer node groups with many nodes over many node groups with few nodes -- this has the biggest impact on CAS scalability
- Use Namespaces for pod isolation instead of dedicated node groups (except in low-trust multi-tenant clusters)
- Use node taints or selectors as the exception, not the rule
- Define regional resources as a single ASG spanning multiple AZs
Key CAS Configuration
# Helm values for Cluster Autoscaler
autoDiscovery:
clusterName: my-cluster
extraArgs:
balance-similar-node-groups: true
skip-nodes-with-local-storage: false
expander: least-waste # or: priority, random, most-pods
scale-down-utilization-threshold: 0.5
scale-down-delay-after-add: 10m
scale-down-unneeded-time: 10m
max-graceful-termination-sec: 600CAS Expander Selection
| Expander | Behavior | Use When |
|---|---|---|
| least-waste | Least idle resources after scale-up | Default -- good for cost |
| priority | User-defined priority order | Prefer specific instance types |
| most-pods | Node fitting the most pending pods | Batch workloads |
| random | Random selection | Testing, simple setups |
Priority Expander Example
Use the priority expander to prefer specific node groups with fallback:
apiVersion: v1
kind: ConfigMap
metadata:
name: cluster-autoscaler-priority-expander
namespace: kube-system
data:
priorities: |-
10:
- .*p2-node-group.*
50:
- .*p3-node-group.*CAS tries p3-node-group first. If provisioning doesn't succeed within --max-node-provision-time (default 15 minutes), it falls back to p2-node-group.
Spot Instances with CAS
Separate On-Demand and Spot capacity into different ASGs because their scheduling properties differ fundamentally (Spot nodes are typically tainted for preemption tolerance).
For MixedInstancePolicies with Spot:
- All instance types must have similar CPU and memory (e.g., m4, m5, m5a, m5n families)
- Use the ec2-instance-selector tool to identify similar instance types
- Maximize diversity across instance families and AZs to reduce interruption impact
- Use
--expander=least-wasteto further optimize cost across diverse node groups
Overprovisioning
CAS adds nodes only when needed, which means pods wait for node launch (~60-90s). For latency-sensitive workloads, use overprovisioning:
Deploy low-priority "pause" pods that occupy spare capacity. When real pods arrive with higher priority, the pause pods are preempted, and the real pods schedule immediately on the existing node. The now-unschedulable pause pods trigger CAS to scale out a new node in the background.
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: overprovisioning
value: -1 # Lower than default (0)
globalDefault: false
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: overprovisioning
spec:
replicas: 3 # One per AZ for optimal zone scheduling
template:
spec:
priorityClassName: overprovisioning
containers:
- name: pause
image: registry.k8s.io/pause
resources:
requests:
cpu: "1"
memory: 2GiSize the pause pods to match your typical workload. Set replicas equal to the number of AZs to ensure spare capacity in each zone.
Scaling from Zero
CAS can scale node groups to and from zero, providing significant cost savings for intermittent workloads. CAS detects node resources from the InstanceType in the LaunchConfiguration or LaunchTemplate.
For pods requiring additional resources, node selectors, or taints not discoverable from the launch config, use ASG tags:
k8s.io/cluster-autoscaler/node-template/resources/$RESOURCE_NAME: 5
k8s.io/cluster-autoscaler/node-template/label/$LABEL_KEY: $LABEL_VALUE
k8s.io/cluster-autoscaler/node-template/taint/$TAINT_KEY: NoScheduleNote: when scaling to zero, capacity is returned to EC2 and may not be available when you scale back up.
Prevent Scale-Down Eviction
For expensive-to-restart workloads (batch jobs, ML training, long test runs), prevent CAS from scaling down the node:
metadata:
annotations:
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"Scalability Tuning
For clusters approaching 1000+ nodes:
- Vertically scale CAS -- increase CPU and memory requests. CAS stores all pods and nodes in memory, which can exceed 1GB for large clusters. Use the Addon Resizer or VPA to automate this.
- Reduce node groups -- fewer, larger node groups improve CAS performance. Many small node groups is a CAS anti-pattern.
- Tune scan interval -- the default 10s scan interval works for most clusters. For large clusters, increasing to 30-60s reduces API call volume (6x fewer calls) with moderate scale-up latency increase. Since node launch takes ~60-90s anyway, a 30s scan interval adds only ~50% to total scale-up time.
- Shard as last resort -- deploy multiple CAS instances, each managing different ASGs. Use separate namespaces to avoid leader election conflicts. Caveat: shards don't communicate, so multiple shards may scale out for the same unschedulable pod.
Advanced Use Cases
EBS Volumes and Stateful Workloads:
- Enable
balance-similar-node-groups=truefor cross-AZ balancing - Configure identical node groups per AZ, each with its own EBS volumes
Accelerators/GPU:
- GPU device plugins can take minutes to advertise resources after node launch, causing repeated unnecessary scale-outs
- Label GPU nodes with
--node-labels k8s.amazonaws.com/accelerator=$ACCELERATOR_TYPEon the kubelet - CAS uses this label to trigger accelerator-optimized behavior (including scale-down of nodes with unused accelerators)
CAS Parameter Reference
| Parameter | Description | Default |
|---|---|---|
scan-interval | How often cluster is evaluated for scaling | 10s |
scale-down-delay-after-add | Cooldown after scale-up before scale-down evaluation | 10m |
scale-down-delay-after-delete | Cooldown after node deletion before scale-down | scan-interval |
scale-down-delay-after-failure | Cooldown after scale-down failure | 3m |
scale-down-unneeded-time | How long a node must be unneeded before scale-down | 10m |
scale-down-unready-time | How long an unready node must be unneeded before removal | 20m |
scale-down-utilization-threshold | Utilization below which a node is considered for scale-down | 0.5 |
max-empty-bulk-delete | Max empty nodes deleted simultaneously | 10 |
max-graceful-termination-sec | Max time for pod graceful shutdown during scale-down | 600 |
---
EKS Auto Mode
What EKS Auto Mode Manages
EKS Auto Mode provides AWS-managed:
- Node provisioning and scaling (no node groups to manage)
- Node OS patching and upgrades
- Compute optimization (instance selection, Spot)
- Cluster add-ons (VPC CNI, CoreDNS, kube-proxy)
When to Use Auto Mode
Use Auto Mode when:
- Minimizing operational overhead is the top priority
- Standard compute patterns (web apps, APIs, microservices)
- Teams without deep Kubernetes node management expertise
- Greenfield clusters where you can adopt Auto Mode from the start
Don't use Auto Mode when:
- You need custom AMIs or specific kernel configurations
- GPU/ML workloads requiring specific instance types or drivers
- Windows containers are required
- You need fine-grained control over node configuration
- Running on EKS Outposts or EKS Anywhere
Auto Mode Configuration
# Terraform
resource "aws_eks_cluster" "this" {
name = "my-cluster"
compute_config {
enabled = true
node_pools = ["general-purpose", "system"]
node_role_arn = aws_iam_role.node.arn
}
kubernetes_network_config {
elastic_load_balancing {
enabled = true
}
}
storage_config {
block_storage {
enabled = true
}
}
}For detailed Auto Mode guidance, see: EKS Auto Mode Reference
---
Horizontal Pod Autoscaler
HPA Configuration Patterns
# CPU-based HPA with scaling behavior
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 3
maxReplicas: 50
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 100
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300 # 5 min cooldown
policies:
- type: Percent
value: 10
periodSeconds: 60 # Scale down 10% per minuteHPA Best Practices
DO:
- Set CPU target to 60-80% (leave headroom for bursts)
- Configure
scaleDown.stabilizationWindowSeconds(prevent flapping) - Set
minReplicas>= 2 for HA (>= 3 for production) - Use
behaviorpolicies to control scaling speed
DON'T:
- Set target utilization to 90%+ (no burst headroom)
- Use HPA without setting resource requests (HPA needs requests for CPU %)
- Combine HPA and VPA on the same CPU/memory metric
- Set
minReplicas: 1for production services
Custom Metrics with CloudWatch
# HPA with custom CloudWatch metric via KEDA or metrics-adapter
metrics:
- type: External
external:
metric:
name: sqs-queue-depth
selector:
matchLabels:
queue: order-processing
target:
type: AverageValue
averageValue: "5"---
Vertical Pod Autoscaler
VPA Modes
| Mode | Behavior | Use When |
|---|---|---|
| Off | Only provides recommendations | Start here -- review before applying |
| Initial | Sets resources only at pod creation | Safe -- no restarts of running pods |
| Auto | Updates resources (may restart pods) | After validating recommendations |
VPA Configuration
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: app-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
updatePolicy:
updateMode: "Off" # Start with recommendations only
resourcePolicy:
containerPolicies:
- containerName: app
minAllowed:
cpu: 100m
memory: 128Mi
maxAllowed:
cpu: 4
memory: 8Gi
controlledResources: ["cpu", "memory"]VPA + HPA Coexistence
Rule: Never scale on the same metric.
Safe combinations:
- VPA manages memory requests + HPA scales on CPU
- VPA manages CPU/memory + HPA scales on custom metrics (QPS, queue depth)
Conflict:
- VPA manages CPU + HPA scales on CPU (fight each other)
---
KEDA Event-Driven Autoscaling
KEDA with SQS
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: sqs-consumer
spec:
scaleTargetRef:
name: queue-processor
minReplicaCount: 0 # Scale to zero when no messages
maxReplicaCount: 100
cooldownPeriod: 300
triggers:
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.us-east-1.amazonaws.com/123456789012/orders
queueLength: "5"
awsRegion: us-east-1
authenticationRef:
name: keda-aws-credentialsCommon KEDA Triggers for EKS
| Trigger | Source | Use Case |
|---|---|---|
| aws-sqs-queue | SQS queue depth | Message processing |
| aws-cloudwatch | CloudWatch metric | Custom metric scaling |
| aws-kinesis-stream | Kinesis shard lag | Stream processing |
| kafka | Kafka consumer lag | Event streaming |
| prometheus | Prometheus query | Custom application metrics |
| cron | Time schedule | Predictive scaling |
---
CoreDNS Autoscaling
Proportional Autoscaler
# Scale CoreDNS proportionally to cluster size
apiVersion: v1
kind: ConfigMap
metadata:
name: dns-autoscaler
namespace: kube-system
data:
linear: |-
{
"coresPerReplica": 256,
"nodesPerReplica": 16,
"min": 2,
"max": 20,
"preventSinglePointFailure": true
}Scaling formula: replicas = max(ceil(cores / coresPerReplica), ceil(nodes / nodesPerReplica))
For a 100-node cluster with 400 cores: max(ceil(400/256), ceil(100/16)) = max(2, 7) = 7 replicas
CoreDNS Tuning
Set lameduck duration to 30 seconds -- delays CoreDNS shutdown during pod termination, allowing iptables rule propagation and in-flight request completion. Especially critical with Karpenter's rapid node churn.
Optimize ndots setting -- default ndots: 5 causes excessive DNS queries (tries multiple search domain suffixes before resolving external names). Set to 2 for pods making external DNS calls:
spec:
dnsConfig:
options:
- name: ndots
value: "2"Alternatively, use trailing dots for FQDNs in application config: api.example.com.
Use NodeLocal DNSCache with Karpenter to prevent DNS failures during node scaling events.
---
Sources:
EKS Cluster Upgrade Best Practices
Part of: eks-best-practices
Purpose: Upgrade planning, in-place and blue-green strategies, add-on management, API deprecation detection, and version support for Amazon EKS
---
Table of Contents
1. Upgrade Planning 2. In-Place Upgrade Procedure 3. Blue-Green Cluster Upgrade 4. Add-On Version Management 5. API Deprecation Detection 6. Data Plane Upgrades 7. Version Support Policy 8. Bottlerocket-Specific Guidance 9. Emergency Rollback Procedures
---
Upgrade Planning
Pre-Upgrade Checklist
| Step | Tool | Action |
|---|---|---|
| 1. Check Cluster Insights | AWS Console / API | Review upgrade readiness insights |
| 2. Detect deprecated APIs | Pluto, kubent, metrics | Scan manifests and cluster for removed APIs |
| 3. Verify add-on compatibility | EKS add-on matrix | Check add-on versions support target K8s |
| 4. Verify infra requirements | AWS CLI | 5+ free IPs in cluster subnets, IAM role exists, KMS key accessible |
| 5. Enable control plane logging | EKS API | Capture logs/errors during upgrade |
| 6. Review version-specific changes | EKS release notes | Check for feature removals (PSP, Dockershim, in-tree storage) |
| 7. Test in non-prod | EKS | Upgrade staging/dev cluster first |
| 8. Verify PDB configuration | kubectl | Ensure PDBs won't block node drains |
| 9. Back up cluster state | Velero / GitOps | Full cluster backup before upgrade |
| 10. Review Karpenter compatibility | Release notes | Verify Karpenter supports target version |
Verify Infrastructure Requirements
AWS requires these resources to complete the control plane upgrade:
# 1. Verify at least 5 free IPs in cluster subnets
CLUSTER=<cluster-name>
aws ec2 describe-subnets --subnet-ids \
$(aws eks describe-cluster --name ${CLUSTER} \
--query 'cluster.resourcesVpcConfig.subnetIds' --output text) \
--query 'Subnets[*].[SubnetId,AvailabilityZone,AvailableIpAddressCount]' \
--output table
# 2. Verify EKS IAM role exists with correct trust policy
ROLE_ARN=$(aws eks describe-cluster --name ${CLUSTER} \
--query 'cluster.roleArn' --output text)
aws iam get-role --role-name ${ROLE_ARN##*/} \
--query 'Role.AssumeRolePolicyDocument'
# Should show: Principal: eks.amazonaws.com, Action: sts:AssumeRole
# 3. If secret encryption is enabled, verify KMS key access
aws eks describe-cluster --name ${CLUSTER} \
--query 'cluster.encryptionConfig'If cluster subnets are running low on IPs, add new subnets in the same AZs via UpdateClusterConfiguration before upgrading. Consider associating additional CIDR blocks to expand the IP pool.
Enable Control Plane Logging
Enable logging before the upgrade to capture any errors during the process:
aws eks update-cluster-config --name my-cluster \
--logging '{"clusterLogging":[{"types":["api","audit","authenticator","controllerManager","scheduler"],"enabled":true}]}'EKS Cluster Insights
# List upgrade insights with issues
aws eks list-insights \
--cluster-name my-cluster \
--filter 'statuses=ERROR,WARNING'
# Get detailed remediation advice for a specific insight
aws eks describe-insight \
--cluster-name my-cluster \
--id <insight-id>
# Returns: affected resources, deprecated APIs, recommended actionsCluster Insights automatically detects:
- Deprecated API usage in your cluster (last 30 days)
- Add-on compatibility issues
- Known upgrade blockers
If an insight shows "status": ERROR, you must resolve it before upgrading.
Upgrade Strategy Decision
| Factor | In-Place Upgrade | Blue-Green Upgrade |
|---|---|---|
| Downtime risk | Minutes (control plane) | Near-zero |
| Rollback | Not possible for control plane | DNS/LB switch back |
| Cost | No extra cost | 2× cluster cost during migration |
| Complexity | Low-Medium | High |
| State migration | None needed | Must migrate PVs, DNS, state |
| Version jump | One minor at a time | Can skip versions (new cluster) |
| Use when | Most upgrades | Critical workloads, major version jumps |
---
In-Place Upgrade Procedure
Upgrade Sequence (Strict Order)
1. Control Plane (AWS-managed, ~15-30 min)
↓
2. EKS Add-ons (VPC CNI, CoreDNS, kube-proxy, EBS CSI)
↓
3. Data Plane (Node groups, Karpenter nodes, or Fargate restart)
↓
4. Custom Add-ons (Ingress controller, cert-manager, monitoring, etc.)
↓
5. Update kubectl (Match client to cluster version)Step 1: Upgrade Control Plane
# Check current version
aws eks describe-cluster --name my-cluster \
--query 'cluster.version'
# Upgrade control plane (one minor version at a time)
aws eks update-cluster-version \
--name my-cluster \
--kubernetes-version 1.31
# Monitor upgrade status
aws eks describe-update \
--name my-cluster \
--update-id <update-id>Key constraints:
- Can only upgrade one minor version at a time (1.29 → 1.30, not 1.29 → 1.31)
- Control plane upgrade takes 15-30 minutes
- API server remains available during upgrade (brief API errors possible)
- Cannot rollback control plane version
Step 2: Upgrade Add-ons
# Check current add-on versions
aws eks describe-addon --cluster-name my-cluster --addon-name vpc-cni
aws eks describe-addon --cluster-name my-cluster --addon-name coredns
aws eks describe-addon --cluster-name my-cluster --addon-name kube-proxy
# Upgrade each add-on
aws eks update-addon \
--cluster-name my-cluster \
--addon-name vpc-cni \
--addon-version v1.18.0-eksbuild.1 \
--resolve-conflicts OVERWRITEVPC CNI constraint: When installed as an EKS managed add-on, VPC CNI can only be upgraded one minor version at a time (same as the cluster).
EKS add-ons are not automatically upgraded during a control plane upgrade — you must initiate each add-on update separately.
Step 3: Upgrade Data Plane
For Managed Node Groups:
aws eks update-nodegroup-version \
--cluster-name my-cluster \
--nodegroup-name default \
--kubernetes-version 1.31
# Monitor rolling update
aws eks describe-nodegroup \
--cluster-name my-cluster \
--nodegroup-name default \
--query 'nodegroup.updateConfig'For EKS Auto Mode: No action needed. After the control plane upgrade, Auto Mode incrementally updates managed nodes while respecting PDBs. Monitor to verify compliance with your operational requirements.
For Karpenter: See Data Plane Upgrades section.
For Fargate: Redeploy workloads to pick up the new version. Identify Fargate pods:
kubectl get pods -A -o wide | grep fargate-
# Restart each deployment running on Fargate
kubectl rollout restart deployment <name> -n <namespace>Step 4: Update kubectl
After the cluster upgrade, update your kubectl client to match:
# Verify kubectl version matches cluster
kubectl version --shortEnsure Availability During Upgrade
Configure PDBs and topology spread to prevent downtime during data plane rolling updates:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: myapp
spec:
minAvailable: "80%"
selector:
matchLabels:
app: myapp
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 10
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
topologySpreadConstraints:
- maxSkew: 2
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: myapp
- maxSkew: 2
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: myappSpreading across zones and hosts ensures pods migrate to new nodes automatically during rolling replacements.
---
Blue-Green Cluster Upgrade
When to Use Blue-Green
- Major version jumps (skipping multiple minor versions via new cluster)
- Zero-downtime requirement for the upgrade itself
- Significant architectural changes alongside version upgrade
- Compliance requirement for rollback capability
Blue-Green Procedure
1. Create "green" cluster at target version
2. Deploy all workloads to green (via GitOps)
3. Run smoke tests on green cluster
4. Shift traffic gradually (DNS weighted / ALB weighted)
5. Monitor for issues
6. Decommission "blue" cluster after validationTraffic Shifting Patterns
| Method | Granularity | Rollback Speed |
|---|---|---|
| Route 53 weighted routing | Percentage-based | Fast (DNS TTL) |
| ALB weighted target groups | Percentage-based | Instant |
| Global Accelerator | Endpoint weights | Instant |
| External DNS cutover | All-or-nothing | DNS TTL dependent |
Blue-Green Downsides to Consider
- API endpoint and OIDC change — all consumers (kubectl, CI/CD, IRSA trust policies) must be updated to the new cluster's endpoint
- Load balancers and external DNS cannot easily span both clusters simultaneously
- 2× cluster cost during the migration period, which may also limit region EC2 capacity
- Dependent workloads need coordination to migrate together (e.g., services that call each other)
- Stateful workloads require backup/restore or shared storage (EFS, managed databases)
Stateful Workload Migration
For workloads with PersistentVolumes: 1. Back up data with Velero or application-level backup 2. Restore in new cluster 3. For EBS: Snapshot → Create volume in new cluster's AZs 4. For EFS: Mount same file system from both clusters 5. For databases: Use managed service (RDS, DynamoDB) — no migration needed
Note: Velero backs up Kubernetes resources and PV data, but not AWS resources (IAM roles, security groups, VPC config). These must be recreated separately (Terraform/CloudFormation).
---
Add-On Version Management
Core EKS Add-Ons
| Add-On | Purpose | Update Priority |
|---|---|---|
| vpc-cni | Pod networking | High — update before node upgrade |
| coredns | Cluster DNS | High — update with control plane |
| kube-proxy | Service networking | High — update with control plane |
| ebs-csi-driver | EBS volumes | Medium — update after control plane |
| efs-csi-driver | EFS volumes | Medium — update after control plane |
| eks-pod-identity-agent | Pod Identity | Medium — update after control plane |
Add-On Compatibility Matrix Check
# List compatible versions for an add-on
aws eks describe-addon-versions \
--addon-name vpc-cni \
--kubernetes-version 1.31 \
--query 'addons[0].addonVersions[*].{Version:addonVersion,Default:compatibilities[0].defaultVersion}' \
--output tableInventory All Components Using K8s API
Before upgrading, identify every component that uses the Kubernetes API directly:
# Find critical cluster components (often in *-system namespaces)
kubectl get ns | grep '-system'Common components to verify compatibility: AWS LBC, Karpenter, Cluster Autoscaler, cert-manager, metrics-server, monitoring agents, ingress controllers, CSI drivers.
Karpenter is tightly coupled to the Kubernetes version — always check Karpenter release notes for target version support.
Cluster Autoscaler must match the cluster minor version — upgrade it when you upgrade the cluster.
Self-Managed Add-On Upgrades
For add-ons not managed by EKS (ingress controllers, cert-manager, etc.): 1. Check the add-on's compatibility matrix for the target K8s version 2. Upgrade the add-on before or after the control plane upgrade (per add-on docs) 3. Test in non-prod first
---
API Deprecation Detection
Detection Methods
| Method | Type | Best For |
|---|---|---|
| EKS Cluster Insights | AWS-managed | Live cluster — first check |
| Prometheus metric | Cluster metric | Continuous monitoring |
| Audit log query | CloudWatch Logs | Historical API usage |
| Pluto | CLI tool | CI/CD pipeline integration |
| kube-no-trouble (kubent) | CLI tool | Quick cluster scan |
| kubectl convert | Built-in | Manual manifest conversion |
Monitor Deprecated API Usage (Prometheus)
The apiserver_requested_deprecated_apis metric (since K8s 1.19) tracks real-time usage of deprecated APIs:
kubectl get --raw /metrics | grep apiserver_requested_deprecated_apis
# Example output:
# apiserver_requested_deprecated_apis{group="policy",removed_release="1.25",
# resource="podsecuritypolicies",version="v1beta1"} 1Query Audit Logs for Deprecated API Calls
CLUSTER="<cluster_name>"
QUERY_ID=$(aws logs start-query \
--log-group-name /aws/eks/${CLUSTER}/cluster \
--start-time $(date -u --date="-30 minutes" "+%s") \
--end-time $(date "+%s") \
--query-string 'fields @message | filter `annotations.k8s.io/deprecated`="true"' \
--query queryId --output text)
sleep 5
aws logs get-query-results --query-id $QUERY_IDUsing Pluto
# Install
brew install FairwindsOps/tap/pluto
# Scan Helm releases in cluster
pluto detect-helm --target-versions k8s=v1.31
# Scan manifest files (more accurate — recommended for CI)
pluto detect-files -d manifests/ --target-versions k8s=v1.31
# Scan live cluster
pluto detect-api-resources --target-versions k8s=v1.31Using kube-no-trouble
sh -c "$(curl -sSL https://git.io/install-kubent)"
kubent --target-version 1.31Scanning static manifests is generally more accurate than live cluster scanning (fewer false positives). Run kubent/pluto in CI pipelines to catch issues before deployment.
Key API Removals by Version
| Version | Removed API | Replacement |
|---|---|---|
| 1.25 | PodSecurityPolicy | Pod Security Admission (PSA) |
| 1.25 | batch/v1beta1 CronJob | batch/v1 |
| 1.25 | Dockershim (CRI) | containerd (EKS Optimized AMI default) |
| 1.26 | flowcontrol.apiserver.k8s.io/v1beta1 | flowcontrol.apiserver.k8s.io/v1beta3 |
| 1.27 | storage.k8s.io/v1beta1 CSIStorageCapacity | storage.k8s.io/v1 |
| 1.29 | flowcontrol.apiserver.k8s.io/v1beta2 | flowcontrol.apiserver.k8s.io/v1 |
| 1.32 | flowcontrol.apiserver.k8s.io/v1beta3 | flowcontrol.apiserver.k8s.io/v1 |
Feature-Specific Migration Guidance
Dockershim removal (1.25): EKS Optimized AMI for 1.25+ uses containerd, not Docker. If you mount the Docker socket (/var/run/docker.sock), detect dependencies with the Detector for Docker Socket (DDS) kubectl plugin before upgrading nodes.
PodSecurityPolicy removal (1.25): Migrate to built-in Pod Security Standards (PSS) or a policy-as-code solution (Kyverno, OPA/Gatekeeper) before upgrading to 1.25.
In-tree storage driver deprecation (1.23): Install the Amazon EBS CSI driver before upgrading to 1.23+ to avoid service interruption for EBS-backed workloads. The in-tree to CSI migration is enabled by default in EKS 1.23+.
Convert Manifests
Use kubectl convert to automatically update API versions in manifest files:
kubectl-convert -f old-deployment.yaml --output-version apps/v1---
Data Plane Upgrades
Version Skew Policy
| Control Plane Version | Supported kubelet Versions | Skew |
|---|---|---|
| ≥ 1.28 | CP version minus 3 (e.g., 1.31 supports kubelet 1.28+) | n-3 |
| < 1.28 | CP version minus 2 (e.g., 1.27 supports kubelet 1.25+) | n-2 |
This applies to MNG, self-managed nodes, and Fargate. However, keep AMI versions current for security — older kubelet versions may have unpatched CVEs.
Karpenter Node Upgrades
Automatic via drift detection:
When you update the control plane version, Karpenter detects AMI drift and automatically replaces nodes:
1. Karpenter detects the node's AMI doesn't match the latest EKS-optimized AMI 2. Karpenter provisions a new node with the updated AMI 3. Karpenter cordons the old node 4. Karpenter drains the old node (respecting PDBs) 5. Pods reschedule on the new node 6. Old node is terminated
Control drain speed with NodePool disruption settings:
spec:
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
budgets:
- nodes: "10%" # Max 10% of nodes disrupted at a time
- nodes: "0"
schedule: "0 9 * * 1-5" # No disruptions during business hours
duration: 8hKarpenter node expiry as an alternative to drift — set expireAfter on the NodePool to automatically replace nodes after a time period, ensuring regular AMI refresh:
spec:
template:
spec:
expireAfter: 720h # 30 days — nodes replaced with latest AMIKarpenter does not add jitter to expiry — configure PDBs to prevent simultaneous expiration from disrupting workloads.
Force immediate node replacement:
kubectl annotate nodes --all karpenter.sh/voluntary-disruption=drifted --overwriteManaged Node Group Upgrades
# Rolling update (default strategy)
aws eks update-nodegroup-version \
--cluster-name my-cluster \
--nodegroup-name default
# Configure update behavior
aws eks update-nodegroup-config \
--cluster-name my-cluster \
--nodegroup-name default \
--update-config '{"maxUnavailable": 1}'
# Or: {"maxUnavailablePercentage": 33}Self-Managed Node Group Upgrades
For nodes deployed outside the EKS managed service, use your provisioning tool:
| Tool | Documentation |
|---|---|
| eksctl | Nodegroup upgrade — supports delete and drain |
| Terraform (EKS Blueprints) | Self-managed node groups |
| kOps | Updates and upgrades |
---
Version Support Policy
EKS Version Lifecycle
| Phase | Patching | Cost | Auto-upgrade? |
|---|---|---|---|
| Standard support | Security + bug fixes | Standard pricing | No |
| Extended support | Critical security only | Additional per-hour surcharge | No |
| End of extended support | None | N/A | Yes — AWS auto-upgrades at a time it chooses |
Always get exact dates from the EKS API — do not compute them from release dates. Standard and extended support windows have historically shifted; the API is the only trustworthy source.
aws eks describe-cluster-versions --region <region> \
--query 'clusterVersions[*].[clusterVersion,status,endOfStandardSupportDate,endOfExtendedSupportDate]' \
--output tableYou can disable extended support so auto-upgrade happens at end of standard support instead.
Planning Timeline
- New K8s minor on EKS: ~3 releases per year
- End of standard support: query the API per cluster version
- End of extended support: query the API per cluster version
- After: EKS auto-upgrades your cluster (may disrupt workloads)
Recommendation: Upgrade every 3-4 months to stay within standard support. Budget one upgrade cycle per quarter. Look beyond the next version — review upcoming K8s releases to identify major changes early (e.g., Dockershim removal was announced well before 1.25).
Additional Upgrade Tools
| Tool | Purpose |
|---|---|
| [ClowdHaus eksup](https://clowdhaus.github.io/eksup/) | CLI to analyze cluster for pre-upgrade issues |
| [GoNoGo](https://github.com/FairwindsOps/GoNoGo) | Determine upgrade confidence for cluster add-ons |
| [eksctl](https://eksctl.io/usage/cluster-upgrade/) | Manage CP, add-ons, and worker node upgrades |
✅ DO:
- Subscribe to EKS version release notifications
- Maintain a documented upgrade runbook
- Test upgrades in non-prod environments first
- Use Cluster Insights to validate readiness
- Enable control plane logging before upgrading
- Use Managed Node Groups, Karpenter, or Auto Mode to simplify data plane upgrades
- Review the EKS release calendar
❌ DON'T:
- Skip more than 2 minor versions behind current
- Rely on extended support as a permanent solution (extra cost, then auto-upgrade)
- Upgrade production without testing in staging first
- Ignore API deprecation warnings from Cluster Insights
- Forget to restart Fargate deployments after control plane upgrade
---
Test Cluster Validation Phase
Before upgrading production, validate the target Kubernetes version in a dedicated test cluster.
9-Step Test Procedure
| Step | Action | Pass Criteria | What It Validates |
|---|---|---|---|
| 1. Deploy test cluster | Create EKS cluster at target K8s version with same config (VPC CNI mode, add-on set, Karpenter/MNG) | Cluster reaches ACTIVE status | Cluster provisioning and configuration compatibility |
| 2. Upgrade control plane (if testing in-place) | Initiate control plane upgrade on test cluster | Control plane upgrade completes without errors | Upgrade process for the specific version transition |
| 3. Verify control plane | Check node status, cluster info, API server responsiveness | All API endpoints healthy, no error responses | API server availability after upgrade |
| 4. Verify add-ons | Check all EKS add-ons and self-managed add-ons are running | All add-on pods Running/Ready, no CrashLoopBackOff | Add-on compatibility with new K8s version |
| 5. Verify workloads | Deploy representative workloads (same Helm charts, same configs) | All deployments reach desired replica count | Application manifest compatibility, scheduling |
| 6. Verify networking | Test ingress, service-to-service communication, DNS resolution, network policies | All connectivity tests pass, DNS resolves within SLA | CNI, CoreDNS, kube-proxy, ingress controller behavior |
| 7. Verify storage | Create PVCs, write/read data, test volume expansion | PVCs bind, data persists across pod restarts | CSI driver compatibility, StorageClass behavior |
| 8. Performance test | Run load test at expected production traffic levels | Latency and throughput within acceptable thresholds | No performance regressions from version change |
| 9. Document results | Record all findings, regressions, and workarounds | Test report reviewed and approved | Formal sign-off for production upgrade |
---
Bottlerocket-Specific Guidance
Bottlerocket Update Operator (BUO)
The Bottlerocket Update Operator automates OS-level updates for Bottlerocket nodes without requiring full node replacement. BUO runs as a DaemonSet (agent on each node) plus a controller that coordinates updates in waves to avoid disrupting too many nodes simultaneously.
| Component | Role |
|---|---|
| brupop-agent | DaemonSet on each Bottlerocket node; checks for updates, applies them |
| brupop-controller | Coordinates update waves, respects PDBs, manages rollout |
| Factor | BUO (OS Update) | Karpenter Drift (Node Replacement) |
|---|---|---|
| What changes | OS packages only | Entire node (new AMI) |
| Disruption | In-place reboot | Pod eviction + new node provisioning |
| Speed | Fast (reboot only) | Slower (provision + schedule + pull images) |
| When to use | Routine OS security patches | K8s version upgrade, AMI change |
| PDB respect | Yes (controller coordinates) | Yes (Karpenter respects PDBs) |
SSM Connectivity Verification
Bottlerocket uses AWS Systems Manager (SSM) for administrative access — there is no SSH. Bottlerocket provides two special containers:
| Container | Purpose | Access Method |
|---|---|---|
| Control container | Limited admin tasks, enabled by default | SSM Session Manager |
| Admin container | Full root access, disabled by default | SSM Session Manager (must enable) |
To verify SSM connectivity: check that the SSM agent is running on the node, the node's IAM role has AmazonSSMManagedInstanceCore policy, and VPC endpoints for SSM are configured (if private subnets without NAT).
OS Update vs K8s Version Upgrade
| Scenario | Action | Tool | Disruption |
|---|---|---|---|
| Security patch for Bottlerocket OS | OS update in-place | BUO | Reboot only |
| New Bottlerocket AMI (same K8s version) | Node replacement | Karpenter drift or MNG update | Pod eviction + reschedule |
| K8s minor version upgrade (e.g., 1.34 → 1.35) | Control plane + data plane upgrade | EKS API + Karpenter drift | Full upgrade sequence |
| Critical CVE requiring immediate patch | OS update (if BUO patch available) or node replacement | BUO or Karpenter | Depends on patch availability |
---
Emergency Rollback Procedures
Rollback Matrix
| Component | Can Rollback? | Method | Notes |
|---|---|---|---|
| EKS control plane | No | Cannot downgrade K8s version | Must rebuild cluster at previous version |
| Data plane nodes | Yes | Replace with previous AMI | Karpenter: update EC2NodeClass AMI; MNG: update launch template |
| EKS managed add-ons | Yes | Revert to previous version via API/Terraform | Some add-ons have minimum version requirements |
| Helm-managed add-ons | Yes | helm rollback or GitOps revert | Check CRD compatibility |
| Application deployments | Yes | kubectl rollout undo or GitOps revert | Verify DB schema compatibility |
| CRD changes | Partial | Can revert CRD spec, but data migration may not reverse | Test CRD rollback in non-prod first |
| Network policies | Yes | Revert via GitOps or kubectl apply | Immediate effect |
| IAM changes | Yes | Revert Terraform/CloudFormation | May take minutes to propagate |
Full Cluster Rebuild from Backup
Use when: catastrophic cluster failure, corrupted etcd state, or failed upgrade with no rollback path.
Prerequisites:
- Velero backups (K8s resources + PV snapshots) in a separate account/region
- GitOps repository with all application and add-on manifests
- Terraform code for cluster infrastructure
- Note: Velero does not back up AWS resources (IAM roles, SGs, VPC config) — these must be recreated via IaC
High-level steps:
| Step | Action | Estimated Time |
|---|---|---|
| 1 | Provision new EKS cluster (Terraform apply) | 15-20 minutes |
| 2 | Install core add-ons (VPC CNI, CoreDNS, Karpenter) | 5-10 minutes |
| 3 | Restore Velero backup (K8s resources) | 10-30 minutes |
| 4 | Restore PV snapshots (EBS volumes) | 10-30 minutes |
| 5 | Reconcile GitOps repository | 5-15 minutes |
| 6 | Validate workloads healthy | 10-15 minutes |
| 7 | Switch DNS/traffic to new cluster | 5 minutes |
| Total | 1-2 hours |
✅ DO:
- Test full cluster rebuild quarterly in an isolated environment
- Keep Terraform state and Velero backups in a separate account
- Document the rebuild runbook with exact commands and validation steps
❌ DON'T:
- Attempt to repair a corrupted cluster for hours — rebuild is often faster
- Skip the validation step before switching traffic
- Forget to update DNS TTLs in advance (low TTL enables faster failover)
---
Sources:
Container Registry Best Practices
Part of: eks-best-practices
Purpose: ECR architecture, operating models, image promotion, vulnerability scanning, base image curation, lifecycle policies, pull-through cache, managed signing, archival storage, and registry configuration for Amazon EKS
---
Table of Contents
1. ECR Architecture 2. Operating Models 3. Image Promotion Pipeline 4. Vulnerability Scanning 5. Base Image Curation 6. ECR Lifecycle Policies 7. Pull-Through Cache 8. Repository Creation Templates 9. Managed Signing 10. Archival Storage Class 11. Registry Configuration
---
ECR Architecture
Private vs Public Repositories
| Type | Use Case | Access |
|---|---|---|
| ECR Private | Internal application images, base images | IAM-authenticated, VPC endpoint supported |
| ECR Public | Open-source projects, shared libraries | Public read, authenticated write |
Repository Naming Conventions
Use a consistent naming pattern that encodes ownership and purpose:
| Pattern | Example | Use When |
|---|---|---|
<team>/<app> | platform/nginx-base, team-a/api-service | Multi-team, clear ownership |
<env>/<app> | prod/api-service, dev/api-service | Environment-separated registries |
<app> (flat) | api-service, web-frontend | Small team, few images |
Cross-Account Access
| Pattern | Mechanism | Use When |
|---|---|---|
| Resource-based policy | ECR repository policy allows cross-account pull | Centralized registry, multiple consumer accounts |
| ECR replication | Automatic replication to target account/region | Each account needs its own copy |
| IAM role assumption | Consumer assumes role in registry account | Fine-grained access control |
VPC Endpoints for ECR
For private clusters or security-sensitive environments, configure VPC endpoints to avoid routing image pulls through the internet:
| Endpoint | Type | Required For |
|---|---|---|
com.amazonaws.<region>.ecr.api | Interface | ECR API calls (auth, describe) |
com.amazonaws.<region>.ecr.dkr | Interface | Docker image pull/push |
com.amazonaws.<region>.s3 | Gateway | Image layer storage (S3-backed) |
---
Operating Models
| Factor | Centralized ECR | Tenant-Managed ECR | Enterprise Registry (Artifactory/Harbor) |
|---|---|---|---|
| Registry location | Single shared AWS account | Each team's own account | Self-hosted or SaaS |
| Who manages | Platform team | Individual teams | Platform/security team |
| Access control | Repository policies + IAM | Per-account IAM | Registry-native RBAC |
| Image promotion | Cross-account replication or re-tag | Push to own registry | Promotion rules in registry |
| Scanning | Centralized Inspector config | Per-account Inspector | Registry-native scanning |
| Best for | Small-medium orgs, single account | Large orgs, strict isolation | Existing enterprise investment, multi-cloud |
When to Use Each
| Scenario | Recommendation |
|---|---|
| Single AWS account, <10 teams | Centralized ECR |
| Multi-account with Control Tower | Centralized ECR in shared services account + cross-account pull |
| Regulatory requirement for team isolation | Tenant-managed ECR |
| Multi-cloud or hybrid | Enterprise registry (Artifactory/Harbor) |
| Air-gapped environment | ECR with pull-through cache or Harbor |
---
Image Promotion Pipeline
Promotion Flow
| Stage | Registry/Tag | Gate | Who Promotes |
|---|---|---|---|
| Build | dev/<app>:git-sha | CI passes (unit tests, lint, scan) | CI pipeline (automatic) |
| Staging | staging/<app>:git-sha | Integration tests pass, scan clean | CI pipeline (automatic) |
| Production | prod/<app>:git-sha | Approval gate, load test pass | Release pipeline (manual approval) |
Tag Strategy
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| Git SHA | api:a1b2c3d | Immutable, traceable to commit | Not human-readable |
| Semantic version | api:1.2.3 | Human-readable, follows convention | Must enforce immutability |
| Git SHA + semver | api:1.2.3-a1b2c3d | Best of both | Longer tag |
| `latest` | api:latest | Convenient | Mutable -- never use in production |
Promotion Methods
| Method | How It Works | Best For |
|---|---|---|
| Re-tag | Add production tag to existing image digest | Same account, fastest |
| Cross-account replication | ECR replicates image to target account | Multi-account, automatic |
| CI pipeline copy | Pipeline pushes image to production registry | Full control, audit trail |
DO:
- Use immutable tags (Git SHA or semver) -- never
latestin production - Enable immutable tag setting on ECR repositories to prevent overwrites
- Include image digest (
@sha256:...) in production deployments for guaranteed immutability
DON'T:
- Use
latesttag in production -- it's mutable and non-deterministic - Rebuild images for promotion -- re-tag or replicate the exact same digest
- Skip scanning between promotion stages
---
Vulnerability Scanning
ECR Scanning Options
| Feature | Basic Scanning | Enhanced Scanning (Inspector) |
|---|---|---|
| Engine | Clair (open-source) | Amazon Inspector |
| Coverage | OS packages only | OS + programming language libraries |
| Trigger | On-push only | Continuous (re-scans on new CVE disclosure) |
| Findings | ECR console only | Security Hub + EventBridge |
| Cost | Free | Per-image pricing |
| Limitation | -- | Cannot scan archived images (must restore first) |
| Recommendation | Development only | Production |
Severity Gating
| Severity | CI Pipeline Action | Production Deploy |
|---|---|---|
| Critical | Block build | Block deploy |
| High | Block build (configurable) | Block deploy |
| Medium | Warn | Allow with exception |
| Low | Log only | Allow |
Integration with Security Hub
Enhanced scanning findings are automatically sent to Security Hub, providing centralized visibility across all accounts. Configure Security Hub automations to:
- Notify teams of critical findings via SNS
- Create Jira/ServiceNow tickets for high findings
- Track remediation SLAs
DO:
- Enable enhanced scanning (Inspector) for production repositories
- Set up continuous scanning -- new CVEs are disclosed daily
- Gate CI/CD pipelines on scan results -- block critical/high before push
- Integrate with Security Hub for centralized finding management
DON'T:
- Rely on basic scanning for production -- it misses language-level vulnerabilities
- Scan only at push time -- images become vulnerable as new CVEs are disclosed
- Ignore medium-severity findings indefinitely -- track and remediate on a schedule
---
Base Image Curation
Why Curate Base Images
Using uncurated public images introduces risk: unknown vulnerabilities, unnecessary packages (shells, curl, build tools), and inconsistent patching. A curated base image pipeline provides a controlled, scanned, and patched foundation for all application images.
Minimal Base Image Options
| Image | Size | Shell | Package Manager | Best For |
|---|---|---|---|---|
| Distroless (Google) | ~2-20 MB | No | No | Production -- minimal attack surface |
| Alpine | ~5 MB | Yes (ash) | apk | Small images, need shell for debugging |
| AL2023-minimal | ~30 MB | Yes (bash) | dnf | AWS-native, Graviton-optimized |
| Ubuntu minimal | ~30 MB | Yes (bash) | apt | Broad compatibility |
| Scratch | 0 MB | No | No | Static binaries (Go, Rust) |
Base Image Pipeline
| Step | Action | Tool |
|---|---|---|
| 1 | Pull upstream base image | CI pipeline |
| 2 | Scan for vulnerabilities | Amazon Inspector / Trivy |
| 3 | Apply security patches | Dockerfile RUN dnf update |
| 4 | Re-scan patched image | Amazon Inspector / Trivy |
| 5 | Push to internal ECR | CI pipeline |
| 6 | Tag as approved base | Semantic version + approved tag |
| 7 | Notify teams of new base | EventBridge + SNS |
Multi-Architecture Images
For Graviton (arm64) support, build multi-arch images using Docker buildx or CI pipeline matrix builds:
| Architecture | Instance Types | Notes |
|---|---|---|
| amd64 | m6i, c6i, r6i | Default, broadest compatibility |
| arm64 | m7g, c7g, r7g (Graviton) | 20-40% cost savings |
| Multi-arch manifest | Both | Single tag works on both architectures |
DO:
- Maintain a curated set of approved base images in a dedicated ECR repository
- Rebuild base images weekly to pick up security patches
- Use multi-stage builds to exclude build tools from final images
- Build multi-arch images if using Graviton
DON'T:
- Pull base images directly from Docker Hub in production -- use pull-through cache or internal copies
- Include shells, curl, or package managers in production images unless required
- Skip scanning base images -- they're the foundation of your security posture
---
ECR Lifecycle Policies
Lifecycle policies automatically clean up old or untagged images, reducing storage costs and keeping repositories manageable.
Recommended Rules
| Rule | Scope | Action | Purpose |
|---|---|---|---|
| Remove untagged images | Untagged | Expire after 1 day | Clean up failed builds |
| Retain N recent tagged | Tagged | Keep last 30 images | Rollback capability |
| Expire old images | Tagged | Expire images older than 90 days | Cost optimization |
| Archive stale images | Tagged | Archive after 180 days | Long-term retention at lower cost |
Count Types
Lifecycle rules support different ways to measure image age:
| Count Type | Counts From | Use When |
|---|---|---|
| sinceImagePushed | Image push date | Default -- expire images that haven't been updated |
| sinceImagePulled | Last pull date | Keep frequently-used images regardless of age |
| sinceImageTransitioned | When image was archived | Manage archived image retention |
Tag Filtering
Use tagPatternList with wildcards to target specific images:
{
"tagStatus": "tagged",
"tagPatternList": ["release-*", "v*"],
"countType": "sinceImagePushed",
"countNumber": 90,
"action": { "type": "expire" }
}This is more flexible than tagPrefixList -- patterns like *-rc or dev-* let you target release candidates, dev builds, or any naming convention.
DO:
- Apply lifecycle policies to every repository -- don't let images accumulate indefinitely
- Keep at least 30 recent tagged images for rollback capability
- Remove untagged images aggressively (1 day retention)
- Use
sinceImagePulledfor shared base images to preserve actively-used versions
DON'T:
- Delete all old images without considering rollback needs
- Apply lifecycle policies that conflict with compliance retention requirements
- Forget to set lifecycle policies on pull-through cache repositories -- they accumulate images quickly
---
Pull-Through Cache
ECR pull-through cache rules automatically cache images from upstream public registries in your private ECR. When a pod pulls an image through the cache, ECR fetches it from the upstream registry, stores it locally, and serves subsequent pulls from the cache.
Supported Upstream Registries
| Registry | Prefix | Auth Required |
|---|---|---|
| Docker Hub | docker.io | Yes (Secrets Manager) |
| ECR Public | public.ecr.aws | No |
| GitHub Container Registry | ghcr.io | Yes (Secrets Manager) |
| Quay.io | quay.io | Yes (Secrets Manager) |
| Kubernetes Registry | registry.k8s.io | No |
| GitLab Container Registry | registry.gitlab.com | Yes (Secrets Manager) |
| Chainguard | cgr.dev | Yes (Secrets Manager) |
| Azure Container Registry | <name>.azurecr.io | Yes (Secrets Manager) |
How It Works
1. Pod requests image via ECR pull-through cache URI (e.g., <acct>.dkr.ecr.<region>.amazonaws.com/docker-hub/library/nginx:1.25) 2. ECR checks if image exists in cache 3. If missing or stale (>24 hours since last check), ECR pulls from upstream -- this requires internet access via NAT gateway or VPC endpoint 4. ECR stores the image (including multi-arch manifests) and serves it locally 5. Subsequent pulls come from cache with no upstream dependency
When to Use
| Scenario | Benefit |
|---|---|
| Docker Hub rate limiting | Avoid 100 pull/6hr anonymous limit |
| Air-gapped environments | Cache images locally, no internet needed after first pull |
| Compliance | All images flow through your ECR with scanning enabled |
| Performance | Faster pulls from regional ECR vs cross-internet |
| Cost | Reduce NAT gateway data transfer costs |
DO:
- Enable pull-through cache for Docker Hub at minimum -- rate limiting is the most common issue
- Store upstream credentials in Secrets Manager for registries that require authentication
- Apply vulnerability scanning and lifecycle policies to cache repositories
- Use repository creation templates to auto-configure cache repositories
DON'T:
- Assume cached images are scanned automatically -- configure scanning rules for cache repositories
- Use pull-through cache as a substitute for curated base images -- it caches everything, including vulnerable images
- Forget that the first pull requires internet access -- air-gapped clusters need initial seeding
---
Repository Creation Templates
Repository creation templates automatically configure new repositories as they're created -- whether through pull-through cache, create-on-push, or replication. Without templates, new repositories get default settings and miss critical configurations like scanning, encryption, and lifecycle policies.
How Templates Work
Templates match repository names by prefix. When a new repository is created (by any mechanism), ECR checks for a matching template and applies its configuration:
| Setting | What It Configures |
|---|---|
| Encryption | KMS key or AES-256 for image layer encryption |
| Image scanning | Basic or enhanced scanning on push |
| Lifecycle policy | Automatic cleanup rules applied at creation |
| Immutability | Tag immutability setting |
| Resource tags | Cost allocation and ownership tags |
| Repository permissions | Cross-account access policies |
Template Matching
Templates use prefix matching with a priority order: 1. Longest matching prefix wins 2. If no prefix matches, the ROOT template applies (if configured)
Example: For repository docker-hub/library/nginx, a template with prefix docker-hub/library/ takes priority over one with prefix docker-hub/.
Create-on-Push
Create-on-push allows repositories to be created automatically when an image is pushed to a repository name that doesn't exist yet. Combined with templates, this means new services can push images without any pre-provisioning -- the repository is created with the correct configuration automatically.
Enable create-on-push either as a registry default or per-template.
DO:
- Create a
ROOTtemplate as a catch-all to ensure every repository gets baseline configuration - Use specific prefix templates for pull-through cache registries (e.g.,
docker-hub/,ghcr/) - Include lifecycle policies in templates so cache repositories don't accumulate images endlessly
- Enable create-on-push for development environments to reduce friction
DON'T:
- Skip templates for pull-through cache -- without them, cached repos have no scanning or lifecycle policies
- Enable create-on-push in production without templates -- you'll get misconfigured repositories
---
Managed Signing
ECR managed signing automatically signs container images on push using AWS Signer, providing cryptographic proof that an image was built and pushed through your pipeline. This supports verification at deploy time via admission controllers like Kyverno or OPA Gatekeeper.
How It Works
1. Configure signing rules at the registry level (up to 10 rules per registry) 2. Each rule specifies a repository filter (prefix match) and an AWS Signer signing profile 3. When an image is pushed to a matching repository, ECR automatically creates a Notation-format signature 4. The signature is stored alongside the image in the same repository 5. Admission controllers verify the signature before allowing the image to run
Configuration
| Setting | Purpose |
|---|---|
| Signing profile | AWS Signer profile that holds the signing key |
| Repository filter | Prefix-based filter (e.g., prod/ signs only production images) |
| Cross-account | Signing profile can be in a different account from the registry |
Integration with Admission Control
Managed signing pairs with Kubernetes admission controllers for deploy-time verification:
| Tool | How It Verifies |
|---|---|
| Kyverno | verifyImages policy checks Notation signatures against trusted signing profiles |
| OPA Gatekeeper | Custom constraint template validates signature presence and signer identity |
| Ratify | External data provider for Gatekeeper, native Notation support |
DO:
- Enable managed signing for production repositories to establish image provenance
- Use repository prefix filters to sign only images that need verification (avoids signing dev/test images)
- Combine with admission controllers to enforce signature verification at deploy time
DON'T:
- Treat signing as a substitute for vulnerability scanning -- signing proves provenance, not safety
- Use the same signing profile for all environments -- separate dev and prod signing identities
See also: Security -- Supply Chain for admission control patterns and image verification policies
---
Archival Storage Class
ECR archival storage provides a low-cost tier for images you need to retain but rarely access -- compliance snapshots, audit artifacts, or old release images. Archival images cost significantly less than standard storage but must be restored before they can be pulled.
How It Works
| Aspect | Detail |
|---|---|
| Transition | Via lifecycle policy archive action, or manual API call |
| Storage cost | Lower than standard ECR storage |
| Restore time | Up to 20 minutes |
| Restore duration | Restored copy available for a configurable number of days |
| Scanning | Archived images cannot be scanned -- restore first |
Lifecycle Policy Integration
Use lifecycle policies to automatically archive images after a retention period:
{
"rules": [
{
"rulePriority": 1,
"selection": {
"tagStatus": "tagged",
"tagPatternList": ["release-*"],
"countType": "sinceImagePushed",
"countNumber": 180
},
"action": { "type": "archive" }
},
{
"rulePriority": 2,
"selection": {
"tagStatus": "tagged",
"tagPatternList": ["release-*"],
"countType": "sinceImageTransitioned",
"countNumber": 730
},
"action": { "type": "expire" }
}
]
}This archives release images after 180 days and permanently deletes them 2 years after archival -- a typical compliance lifecycle.
DO:
- Use archival storage for images required by compliance but rarely pulled
- Chain lifecycle rules: archive after N days, expire after M days from archival
- Test restore times before relying on archived images for disaster recovery
DON'T:
- Archive images you may need for rapid rollback -- 20-minute restore is too slow for incidents
- Forget that archived images can't be scanned -- restore and scan if you need to assess vulnerabilities
---
Registry Configuration
ECR has registry-level settings that affect all repositories in the account/region. Two settings are particularly useful for large registries.
Blob Mounting
Blob mounting allows image layers that already exist in one repository to be referenced (mounted) when pushing to another repository in the same registry, instead of re-uploading them. This is significant when many images share common base layers.
| Setting | Effect |
|---|---|
| Enabled (default) | Push operations mount existing layers from other repos, saving bandwidth and time |
| Disabled | Every push uploads all layers, even if identical copies exist in the registry |
Keep blob mounting enabled unless you have a specific security requirement to isolate layer access between repositories.
Pull-Time Update Exclusions
When pull-through cache is enabled, ECR checks the upstream registry for updates every 24 hours. Pull-time update exclusions let you pin specific repositories so ECR never re-checks upstream -- the cached version is treated as authoritative.
Use this for:
- Known-good images you've validated and don't want upstream changes to override
- Air-gapped environments where you've seeded images and upstream is unreachable
- Compliance scenarios where you need a frozen, auditable copy
---
Helm Chart Management
ECR OCI Support vs S3 Helm Repository
| Factor | ECR OCI Helm Charts | S3-Based Helm Repo (ChartMuseum) |
|---|---|---|
| Protocol | OCI registry (standard) | HTTP(S) Helm repo |
| Authentication | ECR IAM (same as images) | S3 IAM + Helm repo plugin |
| Versioning | OCI tags + digests | Chart index.yaml |
| Replication | ECR cross-account/region replication | S3 replication |
| Scanning | Not applicable (charts are templates) | Not applicable |
| Recommendation | Preferred — native, no extra infra | Legacy or non-AWS Helm consumers |
Pushing and Consuming Helm Charts via ECR
The workflow for Helm charts stored in ECR OCI follows three steps:
1. Authenticate: Obtain an ECR authorization token and pass it to helm registry login. The same ECR IAM credentials used for container images work for Helm charts. 2. Package and push: Package the chart directory into a .tgz archive, then push it to an OCI URI in ECR (e.g., oci://<account-id>.dkr.ecr.<region>.amazonaws.com/charts/). 3. Install from ECR: Reference the OCI URI directly in helm install or in ArgoCD Application source configuration with a specific version tag.
Design considerations:
- Use a dedicated
charts/prefix in ECR to separate Helm charts from container images - Apply the same ECR lifecycle policies to chart repositories to clean up old versions
- ECR cross-account replication works for Helm charts — spoke accounts get chart replicas automatically
- ArgoCD natively supports OCI Helm sources — no extra configuration needed beyond ECR auth
---
Sources:
EKS Cost Optimization
Part of: eks-best-practices
Purpose: Cost optimization framework, compute/networking/storage cost strategies, observability cost management, tagging, and cost visibility tools for Amazon EKS
---
Table of Contents
1. Cost Optimization Framework 2. Compute Cost Optimization 3. Networking Cost Optimization 4. Storage Cost Optimization 5. Observability Cost Optimization 6. Tagging & Cost Visibility
---
Cost Optimization Framework
AWS Cloud Financial Management (CFM) organizes cost optimization into four pillars:
| Pillar | Focus | EKS Actions |
|---|---|---|
| See | Measurement & accountability | Tag resources, deploy Kubecost, enable Cost Explorer |
| Save | Eliminate waste, optimize purchasing | Right-size, Spot/Graviton, consolidation |
| Plan | Forecast & budget | Track unit economics (cost per request/transaction) |
| Run | Continuous improvement | FinOps flywheel -- iterate on See/Save/Plan |
The "See" pillar comes first because you can't optimize what you can't measure. Start with tagging and cost visibility before pursuing compute or networking savings.
EKS Cost Components
| Component | Cost Driver | Optimization Lever |
|---|---|---|
| EKS control plane | $0.10/hour per cluster | Fewer clusters, multi-tenant |
| EC2 instances | Instance type + hours | Right-sizing, Spot, Graviton |
| EBS volumes | Volume type + size + IOPS | gp3, right-size, cleanup unused |
| Data transfer | Cross-AZ, internet egress | Topology-aware routing, VPC endpoints |
| Load balancers | Per ALB/NLB + LCU/hour | Consolidate ingress, shared ALB |
| NAT Gateway | Per GB processed + hourly | VPC endpoints for AWS services |
| Observability | Log ingestion + metric storage | Filter, retain selectively, reduce cardinality |
Quick Wins
| Action | Typical Savings | Effort |
|---|---|---|
| Switch to Graviton (arm64) | 20-40% | Low -- rebuild images for arm64 |
| Use Spot for non-critical | 60-90% | Low -- Karpenter handles fallback |
| Enable Karpenter consolidation | 20-30% | Low -- enable in NodePool |
| Right-size with VPA recommendations | 15-30% | Medium -- review and apply |
| Use gp3 instead of gp2 | 20% on EBS | Low -- update StorageClass |
| VPC endpoints for ECR/S3 | Eliminate NAT costs | Low -- one-time setup |
| Topology-aware routing | 50-80% on cross-AZ | Medium -- enable topology hints |
| Reduce log verbosity in prod | 30-50% on logging | Low -- adjust log levels |
---
Compute Cost Optimization
Compute is typically the largest cost driver for EKS. Optimize in this order:
1. Right-size workloads -- match requests to actual usage 2. Reduce unused capacity -- autoscale and consolidate 3. Optimize capacity types -- Spot, Graviton, Savings Plans
Right-Sizing Workloads
Requests should align with actual utilization. Overprovisioned requests waste capacity -- the largest factor in total cluster costs. Each container (including sidecars) should have its own requests and limits.
Right-sizing tools:
| Tool | Approach | Best For |
|---|---|---|
| VPA (recommendation mode) | Historical usage analysis | Per-deployment recommendations |
| Goldilocks | VPA-based dashboard | Cluster-wide visibility |
| KRR (Robusta) | Prometheus-based analysis | Quick right-sizing across namespaces |
| Kubecost | Cost-aware recommendations | Tying resource changes to dollar savings |
# Deploy VPA in recommendation-only mode
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: app-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
updatePolicy:
updateMode: "Off" # Recommendations only# View recommendations
kubectl get vpa app-vpa -o jsonpath='{.status.recommendation.containerRecommendations[*]}' | jqRight-sizing decision framework:
| Signal | Action |
|---|---|
| CPU request >> actual usage (consistently) | Reduce CPU request to P95 usage |
| Memory request >> actual usage | Reduce memory request to P99 usage + 20% buffer |
| CPU throttling observed | Increase CPU request (or remove CPU limit) |
| OOMKilled events | Increase memory limit |
| Pod pending due to resources | Scale nodes or reduce requests |
Reducing Unused Capacity
Use HPA to scale pods based on demand, then let node autoscalers remove empty or underutilized nodes. Restrictive PodDisruptionBudgets can block node scale-down -- set minAvailable well below your replica count (e.g. minAvailable: 4 for a 6-pod deployment).
For event-driven scaling (SQS queues, Kafka, CloudWatch metrics), use KEDA instead of HPA's built-in metrics.
Karpenter Consolidation
Karpenter continuously monitors and bin-packs workloads onto fewer, optimally-sized instances:
# Enable consolidation in NodePool
spec:
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 30s # Fast for non-prod; use longer (e.g. 5m) in prodKarpenter selects the most cost-effective instance from your allowed types. It replaces underutilized nodes with smaller ones and removes empty nodes automatically.
For workloads that shouldn't be interrupted (long batch jobs without checkpointing), use the karpenter.sh/do-not-disrupt: "true" annotation.
See also: Karpenter Reference for detailed NodePool configuration, consolidation tuning, and Spot handling.
Cluster Autoscaler Priority Expander
If using Cluster Autoscaler instead of Karpenter, the priority expander lets you prefer cheaper capacity:
# Priority expander ConfigMap -- scale reserved/Spot groups before on-demand
apiVersion: v1
kind: ConfigMap
metadata:
name: cluster-autoscaler-priority-expander
namespace: kube-system
data:
priorities: |-
10:
- .*ondemand.*
50:
- .*reserved.*Also consider the Kubernetes Descheduler alongside CAS -- it rebalances pod placement after scheduling to improve cluster-wide utilization, which CAS alone does not do.
Graviton (arm64) Migration
Graviton instances deliver 20-40% better price/performance than equivalent x86:
# Karpenter NodePool supporting both architectures
spec:
template:
spec:
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"] # Karpenter prefers cheaper Graviton
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]DO:
- Build multi-arch container images (
docker buildx) - Test on arm64 in staging before production
- Use Karpenter -- it automatically selects the most cost-effective architecture
DON'T:
- Assume all container images support arm64 (check base images)
- Mix architectures within a single deployment without affinity rules
Spot Instance Strategies
# Karpenter: Diversified Spot strategy
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"] # Multiple families
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["5"] # Current gen only
- key: karpenter.k8s.aws/instance-size
operator: In
values: ["large", "xlarge", "2xlarge"] # Multiple sizesSpot suitability:
| Workload Type | Spot Suitable? | Notes |
|---|---|---|
| Stateless web/API | Yes | Use with PDBs + multi-AZ |
| Batch processing | Yes | Ideal -- tolerant of interruption |
| CI/CD runners | Yes | Short-lived, easily retried |
| Development/test | Yes | Cost savings, acceptable disruption |
| Databases/stateful | No | Use On-Demand for data safety |
| Single-replica critical | No | No fallback on interruption |
| Long-running ML training | Maybe | Use checkpointing + Spot |
Karpenter handles Spot interruptions automatically (receives 2-min notice, cordons, launches replacement, drains respecting PDBs). For MNG/self-managed nodes, deploy AWS Node Termination Handler.
Savings Plans & Reserved Instances
For stable, predictable baseline capacity, Compute Savings Plans provide up to 66% savings over On-Demand. Layer them with Spot for variable workloads:
- Baseline (always running): Savings Plans or Reserved Instances
- Variable (scales up/down): Spot with On-Demand fallback
Downscaling Patterns
# KEDA cron-based scaling -- scale to zero at night
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: dev-app
spec:
scaleTargetRef:
name: dev-app
minReplicaCount: 0
maxReplicaCount: 5
triggers:
- type: cron
metadata:
timezone: America/New_York
start: "0 8 * * 1-5" # Scale up at 8 AM weekdays
end: "0 20 * * 1-5" # Scale down at 8 PM weekdays
desiredReplicas: "3"When all pods are evicted from a node, Karpenter removes it. Combined with HPA/KEDA scaling pods to zero, nodes automatically scale to zero.
---
Networking Cost Optimization
Cross-AZ data transfer is a significant cost in multi-AZ EKS clusters. AWS charges for data crossing AZ boundaries, so keeping traffic local reduces costs.
Pod-to-Pod Traffic: Topology-Aware Routing
By default, kube-proxy distributes traffic across all pods regardless of AZ placement, causing cross-AZ charges.
Topology-aware routing (beta) allocates endpoints proportionally across zones:
apiVersion: v1
kind: Service
metadata:
name: orders-service
annotations:
service.kubernetes.io/topology-mode: Auto
spec:
selector:
app: orders
type: ClusterIPWorks best with evenly distributed workloads. Use with pod topology spread constraints to keep replicas balanced across zones. Hints may not be assigned when capacity fluctuates across zones (e.g., with Spot instances).
Traffic Distribution (GA in K8s 1.33) is a simpler, more predictable alternative:
apiVersion: v1
kind: Service
metadata:
name: orders-service
spec:
trafficDistribution: PreferClose
selector:
app: orders
type: ClusterIPPreferClose routes to same-zone endpoints first, falling back to any endpoint when none are local. Can overload endpoints in high-traffic zones -- mitigate with per-zone deployments with independent HPAs, or topology spread constraints.
Service Internal Traffic Policy restricts traffic to the originating node:
spec:
internalTrafficPolicy: LocalUse for tightly coupled services with frequent inter-communication. Requires co-located replicas via pod affinity rules -- traffic is dropped when no local endpoint exists. Cannot be combined with topology-aware routing.
Load Balancer to Pod Communication
The AWS Load Balancer Controller supports two traffic modes:
| Mode | Path | Cross-AZ Cost |
|---|---|---|
| Instance mode | LB -> NodePort -> kube-proxy -> Pod | Likely cross-AZ hops |
| IP mode | LB -> Pod directly | No extra hops |
Use IP mode to eliminate data transfer charges from LB-to-Pod traffic. Ensure the LB is deployed across all subnets in your VPC.
Network Cost Quick Reference
| Strategy | Savings | Effort |
|---|---|---|
| IP mode on ALB/NLB | Eliminates LB-to-Pod cross-AZ charges | Low |
| Topology-aware routing / Traffic Distribution | Reduces cross-AZ pod-to-pod traffic | Medium |
| Gateway VPC endpoints (S3, DynamoDB) | Free -- no hourly or data transfer cost | Low |
| Interface VPC endpoints (ECR, STS) | Avoids NAT Gateway data processing ($0.045/GB) | Low |
| NAT Gateway per AZ | Eliminates inter-AZ NAT traversal | Low |
| In-region ECR pulls | Free (vs cross-region data transfer) | Low |
For detailed networking configuration, see: Networking Reference | Networking -- Ingress & DNS
---
Storage Cost Optimization
Ephemeral Storage
| Option | Cost | Best For |
|---|---|---|
| gp3 root volume | ~20% less than gp2 | Default choice for node root volumes |
| EC2 instance stores | No additional cost | Caches, scratch space, temporary data |
Instance stores are physically attached to the host -- free, but data is lost on termination. Use HostPath or the Local Persistent Volume Static Provisioner to expose them in Kubernetes.
Persistent Volumes: EBS
Start with gp3 -- 20% cheaper per GB than gp2 and allows independent IOPS/throughput scaling:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gp3
provisioner: ebs.csi.aws.com
parameters:
type: gp3
fsType: ext4
volumeBindingMode: WaitForFirstConsumerMigration paths from gp2 to gp3:
| Method | Downtime | Requires |
|---|---|---|
| CSI Volume Snapshots (backup + restore) | Yes | EBS CSI driver |
| PVC annotation modification | No | EBS CSI driver >= v1.19 |
| VolumeAttributesClass API | No | EKS >= 1.31 + EBS CSI >= 1.35 |
For mission-critical workloads needing >16K IOPS or >1 GiB/s throughput, use io2 Block Express (up to 256K IOPS, 4 GiB/s, 64 TiB).
Dynamically resize volumes as data grows rather than overprovisioning upfront. Use AWS Trusted Advisor or Popeye to find dangling/unused volumes.
Persistent Volumes: EFS
EFS charges only for stored data with no upfront provisioning. Use Intelligent-Tiering to automatically move infrequently accessed files to cheaper storage (up to 92% savings).
| Storage Class | Cost | Use When |
|---|---|---|
| EFS Standard | Highest | Frequently accessed, multi-AZ |
| EFS Standard-IA | ~92% less | Infrequently accessed, multi-AZ |
| EFS One Zone | ~47% less than Standard | Single-AZ tolerance, frequent access |
| EFS One Zone-IA | Lowest | Single-AZ, infrequent access |
EFS lifecycle policies and Intelligent-Tiering must be configured outside the CSI driver (console or EFS API).
Persistent Volumes: FSx
| Option | Best For | Key Advantage |
|---|---|---|
| FSx for Lustre | ML training, HPC, video processing | Sub-ms latency, hundreds of GB/s throughput |
| FSx for NetApp ONTAP | Multi-protocol (NFS/SMB/iSCSI) | Data tiering between SSD and capacity pool |
For FSx for Lustre, link to S3 for long-term storage -- lazy-load data into Lustre for processing, write results back to S3, then delete the filesystem.
Storage Quick Reference
| Strategy | Savings |
|---|---|
| gp3 over gp2 | 20% lower $/GB, independent IOPS/throughput |
| EFS Intelligent-Tiering | Up to 92% on infrequently accessed files |
| Instance store for caches | Zero additional cost (ephemeral) |
| Container image optimization | Distroless/scratch base images, multi-stage builds |
| Clean up dangling volumes | Direct savings -- Popeye or AWS Trusted Advisor |
| EBS snapshot retention policy | Avoid unbounded snapshot growth via DLM or Velero TTL |
---
Observability Cost Optimization
Observability costs scale with data volume. Optimize by collecting only what matters and retaining intelligently.
Logging
Control plane logs: Evaluate which log types are needed per environment. Non-production clusters may only need API server logs enabled selectively. Production clusters benefit from all types for incident investigation. EKS control plane logs are classified as Vended Logs with volume discount pricing.
| Strategy | Impact |
|---|---|
| Selective log types per environment | Reduce ingestion volume |
| Stream to S3 via CloudWatch subscriptions | Cheaper long-term storage |
| Forward non-critical logs directly to S3 (FluentBit) | Skip CloudWatch entirely |
| Reduce log levels (ERROR in prod, DEBUG in dev) | Significant volume reduction |
| Filter Kubernetes metadata in FluentBit | Remove unnecessary enrichment |
Metrics
| Strategy | Impact |
|---|---|
| Monitor only what matters (work backwards from KPIs) | Fewer metrics = lower storage cost |
| Reduce cardinality (drop unnecessary labels) | Fewer unique time series |
| Tune scrape intervals (15s -> 30s/60s for non-critical) | 50-75% fewer data points |
| Use recording rules for pre-aggregation | Replace high-cardinality queries |
Identify high-cardinality offenders:
# Top 5 scrape targets by metric count
topk_max(5, max_over_time(scrape_samples_scraped[1h]))
# Top 5 by churn rate (new series created per scrape)
topk_max(5, max_over_time(scrape_series_added[1h]))Use Grafana Mimirtool to find metrics collected but never used in dashboards or alerts.
Traces
For high-volume services, implement sampling strategies:
- Head-based sampling: Decide at trace start (simple, but may miss important traces)
- Tail-based sampling: Decide after trace completes (captures errors/slow requests, more complex)
Use the ADOT Collector's tail sampling processor to retain only traces that exceed latency thresholds or contain errors.
---
Tagging & Cost Visibility
Tagging Strategy
Tags are the foundation of cost allocation. Without them, you can see total spend but not who's spending it.
# Karpenter EC2NodeClass tags
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
tags:
Team: platform
CostCenter: engineering
Environment: production
ManagedBy: karpenter
kubernetes.io/cluster/my-cluster: owned| Resource | Tag Source | Notes |
|---|---|---|
| EC2 instances | NodePool/EC2NodeClass tags | Karpenter applies automatically |
| EBS volumes | StorageClass tags | Set tagSpecification in CSI driver |
| ALB/NLB | Service/Ingress annotations | Via AWS LBC |
| VPC endpoints | Terraform/CloudFormation | Tag at creation |
AWS resource tags don't directly correlate with Kubernetes labels. Use Kubernetes labels on pods/namespaces for in-cluster cost attribution (via Kubecost), and AWS tags for billing/Cost Explorer views.
Cost Visibility Tools
| Tool | Scope | Cost | Best For |
|---|---|---|---|
| AWS Cost Explorer | Account-level | Free | High-level trends, SP/RI recommendations |
| Kubecost | Cluster-level | Free (open source) | Per-namespace/pod cost allocation |
| CloudWatch Container Insights | Cluster + pod | ~$0.30/container/month | Resource utilization monitoring |
| AWS Billing + tags | Account-level | Free | Chargeback by team/project |
| Karpenter metrics | Node-level | Free | Consolidation efficiency |
Kubecost
Kubecost provides real-time cost monitoring, namespace/label allocation, and right-sizing recommendations:
helm install kubecost kubecost/cost-analyzer \
--namespace kubecost --create-namespace \
--set kubecostProductConfigs.clusterName=my-cluster \
--set kubecostProductConfigs.cloudIntegrationSecret=cloud-integration| Feature | Free | Enterprise |
|---|---|---|
| Namespace/label cost allocation | Yes | Yes |
| Right-sizing recommendations | Yes | Yes |
| Idle cost detection | Yes | Yes |
| Multi-cluster | No | Yes |
| SSO/RBAC | No | Yes |
| Long-term storage | 15 days | Unlimited |
DO:
- Use namespace-level cost allocation for multi-tenant clusters
- Enable CUR integration for accurate AWS pricing (not list price estimates)
- Set up idle cost alerts -- unused resources are the biggest waste
- Use right-sizing recommendations to adjust resource requests
DON'T:
- Rely solely on CloudWatch for K8s cost attribution -- it lacks namespace-level granularity
- Skip resource requests on pods -- Kubecost needs requests to calculate allocation
- Ignore shared costs (control plane, monitoring, ingress) -- allocate proportionally
---
Sources:
Related skills
FAQ
What is the default node autoscaler recommendation?
Karpenter, described as the best balance of flexibility, cost, and automation for new clusters.
Does it cover Terraform?
Yes, it answers configuration questions about the terraform-aws-modules/terraform-aws-eks module and its examples.