Amazon EKS Best Practices: Cost, Security, and Performance
Running production workloads on Amazon Elastic Kubernetes Service (EKS) requires more than just provisioning a cluster and deploying containers. Without deliberate optimization, costs can spiral, security gaps can widen, and application performance can degrade under load. This comprehensive tutorial walks you through the three critical pillars of EKS operations — cost efficiency, security hardening, and performance tuning — with practical, production-tested code examples you can apply immediately.
What This Tutorial Covers
We'll explore each pillar in depth, starting with the core concepts and moving through concrete implementation patterns. Every section includes deployable code snippets, CLI commands, and configuration manifests that you can adapt to your own clusters. By the end, you'll have a repeatable framework for operating EKS clusters that are lean, locked down, and lightning-fast.
Part 1: Cost Optimization on EKS
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Why Cost Optimization Matters on EKS
EKS charges a base fee of $0.10 per hour per cluster, plus the underlying EC2 or Fargate compute costs. Without careful management, teams commonly overspend by 30-50% due to over-provisioned instances, idle capacity, and inefficient pod scheduling. Cost optimization isn't about cutting corners — it's about extracting maximum value from every dollar spent on infrastructure.
1. Right-Sizing with Resource Requests and Limits
The foundation of cost control is accurately declaring what each pod needs. Kubernetes uses requests for scheduling decisions and limits for runtime enforcement. Setting these correctly prevents both over-provisioning and noisy-neighbor problems.
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: app
image: myapp/order-service:2.4.1
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1Gi"
Best Practice: Start by setting requests equal to the average observed usage over a week, and limits at 2-3x requests. Use metrics from your monitoring stack — never guess. Adjust quarterly as traffic patterns evolve.
2. Cluster Autoscaler and Karpenter for Dynamic Node Scaling
Static node groups waste money during low-traffic periods. Cluster Autoscaler and the more modern Karpenter automatically adjust the node count based on pending pod demands.
Installing Karpenter via Helm (recommended for new clusters):
# Add the Karpenter Helm repository
helm repo add karpenter https://charts.karpenter.sh
helm repo update
# Install Karpenter with IAM role for node provisioning
helm upgrade --install karpenter karpenter/karpenter \
--namespace karpenter \
--create-namespace \
--set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="arn:aws:iam::123456789012:role/KarpenterNodeRole" \
--set settings.aws.defaultInstanceProfile="KarpenterNodeInstanceProfile" \
--set settings.aws.clusterName="my-eks-cluster" \
--set settings.aws.clusterEndpoint="https://...eks.amazonaws.com" \
--wait
Karpenter Provisioner example — targeting cheap Spot instances with fallback to On-Demand:
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
name: cost-optimized
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: node.kubernetes.io/instance-type
operator: In
values: ["t3a.medium", "t3a.large", "m6i.large", "m6i.xlarge"]
limits:
resources:
cpu: "2000"
memory: "8000Gi"
consolidation:
enabled: true
ttlSecondsAfterEmpty: 300
Best Practice: Use Spot instances for stateless, fault-tolerant workloads (batch jobs, web tiers) and On-Demand for stateful services (databases, message queues). Karpenter's consolidation feature automatically replaces inefficient node groups with fewer, better-packed nodes.
3. Horizontal Pod Autoscaler (HPA) for Workload-Level Scaling
While node-level scaling handles infrastructure, HPA adjusts pod replicas based on actual CPU or custom metrics. This prevents running more pods than necessary during low-traffic windows.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: checkout-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: checkout-service
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 65
- type: Pods
pods:
metric:
name: requests_per_second
target:
type: AverageValue
averageValue: "1000"
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 30
periodSeconds: 60
Best Practice: Always set scaleDown.stabilizationWindowSeconds to at least 300 (5 minutes) to prevent thrashing during brief traffic dips. Combine CPU metrics with application-specific metrics like request latency or queue depth for smarter scaling decisions.
4. Intelligent Pod Scheduling with Affinity and Bin Packing
Kubernetes' default scheduler spreads pods across nodes for availability, but this often leaves stranded capacity on each node. Using pod affinity and anti-affinity strategically lets you pack pods onto fewer nodes without sacrificing resilience.
apiVersion: apps/v1
kind: Deployment
metadata:
name: batch-processor
spec:
replicas: 8
template:
spec:
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
podAffinityTerm:
labelSelector:
matchLabels:
workload-type: batch
topologyKey: kubernetes.io/hostname
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: lifecycle
operator: In
values: ["spot-compute"]
containers:
- name: processor
resources:
requests:
cpu: "2"
memory: "4Gi"
Best Practice: Use preferredDuringScheduling (soft affinity) for co-locating related services to reduce inter-node traffic costs, and requiredDuringScheduling (hard affinity) only when absolutely necessary, as it can prevent scheduling during resource crunches.
5. Fargate for Workloads with Irregular Traffic Patterns
For workloads that don't run 24/7 — like CI/CD runners, data processing pipelines, or internal tools — Fargate eliminates the cost of idle EC2 instances entirely. You pay only for the vCPU and memory consumed by active pods.
apiVersion: v1
kind: Pod
metadata:
name: data-export-job
namespace: data-pipeline
spec:
nodeSelector:
eks.amazonaws.com/compute-type: fargate
containers:
- name: export
image: myapp/data-exporter:latest
resources:
requests:
cpu: "2"
memory: "8Gi"
Best Practice: Create a dedicated Fargate profile for each namespace that runs sporadic workloads. The 4 vCPU / 30 GB memory limit per Fargate pod means you should break large jobs into parallel smaller pods when possible.
Part 2: Security Hardening on EKS
Why Security on EKS Demands Layered Defenses
A Kubernetes cluster is a complex system with multiple attack surfaces: the control plane, worker nodes, the container runtime, networking, and the supply chain of container images. EKS secures the managed control plane by default, but you are responsible for everything else under the shared responsibility model. A single misconfigured IAM role or overly permissive network policy can expose your entire application estate.
1. IAM Roles for Service Accounts (IRSA) — The Golden Standard
IRSA eliminates the need to pass AWS credentials into pods via environment variables or files. Instead, pods assume IAM roles directly through a Kubernetes service account annotation, with credentials automatically rotated by the EKS pod identity mechanism.
Step 1: Create an OIDC-compatible IAM role with a trust policy:
# First, retrieve the OIDC issuer URL for your cluster
aws eks describe-cluster --name my-cluster --query "cluster.identity.oidc.issuer" --output text
# Create trust policy file
cat > trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DB1D3F36E6A9921AA2F"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DB1D3F36E6A9921AA2F:sub": "system:serviceaccount:production:payment-service-sa"
}
}
}
]
}
EOF
aws iam create-role --role-name payment-service-dynamodb-role \
--assume-role-policy-document file://trust-policy.json
# Attach the DynamoDB read policy
aws iam attach-role-policy --role-name payment-service-dynamodb-role \
--policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess
Step 2: Create the Kubernetes service account and annotate it:
apiVersion: v1
kind: ServiceAccount
metadata:
name: payment-service-sa
namespace: production
annotations:
eks.amazonaws.com/role-arn: "arn:aws:iam::123456789012:role/payment-service-dynamodb-role"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
namespace: production
spec:
template:
spec:
serviceAccountName: payment-service-sa
containers:
- name: app
image: myapp/payment-service:3.1.0
Best Practice: Never share IAM roles across unrelated services. Create a dedicated role per microservice with the principle of least privilege — only the specific actions on specific resources that service actually needs. Use IAM Access Analyzer to validate policies.
2. Pod Security Standards (PSS) — Replacing PSPs
Pod Security Policies (PSPs) are deprecated as of Kubernetes 1.25. The replacement is the built-in Pod Security Standards, enforced via namespace labels. These prevent containers from running as root, mounting host filesystems, or using privileged mode.
# Enforce the restricted baseline on a production namespace
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted
# Verify the label
kubectl get namespace production -o yaml
Pod manifest compliant with the 'restricted' profile:
apiVersion: v1
kind: Pod
metadata:
name: secure-app
namespace: production
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp/secure-app:latest
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
Best Practice: Apply restricted to all production namespaces. Use baseline only for namespaces that genuinely need host volumes or specific capabilities (like logging agents). Audit existing pods with kubectl label --dry-run before enforcing.
3. Network Policies — East-West Traffic Isolation
By default, all pods in a cluster can communicate freely. Network Policies define explicit allow rules, isolating services and limiting blast radius during a compromise.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: payment-network-policy
namespace: production
spec:
podSelector:
matchLabels:
app: payment-service
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: gateway-service
namespaceSelector:
matchLabels:
namespace: production
ports:
- protocol: TCP
port: 8443
egress:
- to:
- podSelector:
matchLabels:
app: redis
namespaceSelector:
matchLabels:
namespace: data-layer
ports:
- protocol: TCP
port: 6379
- to:
- ipBlock:
cidr: 169.254.0.0/16
except:
- 169.254.0.0/24
ports:
- protocol: TCP
port: 443
Best Practice: Start with a default-deny ingress policy in every namespace, then explicitly allow only the communication paths your architecture diagram shows. Use namespace labels for cross-namespace policies to avoid hard-coding namespace names. The Calico network plugin (available via EKS add-on) provides richer policy capabilities than the default AWS VPC CNI.
4. Secrets Management with AWS Secrets Manager CSI Driver
Kubernetes Secrets are stored as base64-encoded strings in etcd — they are not encrypted by default. The AWS Secrets Manager CSI Driver mounts secrets as files in a pod's filesystem, with automatic rotation and no exposure in the Kubernetes API.
# Install the CSI driver via Helm
helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \
--namespace kube-system
# Install the AWS provider
kubectl apply -f https://raw.githubusercontent.com/aws/secrets-store-csi-driver-provider-aws/main/deployment/aws-provider-installer.yaml
# Create a SecretProviderClass that maps AWS secrets to files
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: database-credentials
namespace: production
spec:
provider: aws
parameters:
objects: |
- objectName: "prod/database/credentials"
objectType: "secretsmanager"
jmesPath:
- path: "username"
objectAlias: "DB_USERNAME"
- path: "password"
objectAlias: "DB_PASSWORD"
# Mount the secret in a Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
template:
spec:
containers:
- name: app
image: myapp/api-server:4.0.2
volumeMounts:
- name: secrets
mountPath: "/mnt/secrets"
readOnly: true
env:
- name: DB_USERNAME_FILE
value: "/mnt/secrets/DB_USERNAME"
- name: DB_PASSWORD_FILE
value: "/mnt/secrets/DB_PASSWORD"
volumes:
- name: secrets
csi:
driver: secrets-store.csi.secrets-store.csi.x-k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "database-credentials"
Best Practice: Enable automatic rotation by setting a rotation schedule in AWS Secrets Manager. The CSI driver will refresh mounted secrets within a configurable polling interval. Never log the contents of /mnt/secrets — configure your logging agent to exclude that path.
5. Image Security — Scanning and Admission Control
Running unverified container images is a primary vector for supply chain attacks. Implement image scanning in your CI pipeline and enforce admission policies that reject non-compliant images.
Using Trivy for image scanning in CI (GitHub Actions example):
# .github/workflows/scan.yml
name: Container Image Scan
on:
push:
branches: [main]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:scan-target .
- name: Run Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:scan-target
format: sarif
output: trivy-results.sarif
severity: "CRITICAL,HIGH"
exit-code: 1
Best Practice: Use ECR's built-in scanning (enhanced scan with Inspector) as a second layer. Configure ECR repository policies to reject image pushes that contain critical vulnerabilities. Combine with OPA/Gatekeeper admission webhooks on the cluster to enforce that only images from your approved ECR repositories can run.
Part 3: Performance Optimization on EKS
Why Performance Tuning Is a Continuous Process
Performance in Kubernetes is not a one-time configuration task. Traffic patterns shift, new services are deployed, and node types evolve. A cluster that performs well today can develop latency spikes or scheduling bottlenecks within weeks. Proactive tuning keeps your applications responsive and your users satisfied.
1. Pod Resource Requests — The Scheduling Foundation
Accurate resource requests are the single most important performance lever. Requests that are too low cause CPU throttling and OOM kills; requests that are too high waste compute and prevent efficient scheduling.
# Benchmark a pod's actual resource usage over 24 hours
kubectl create deployment nginx-bench --image=nginx --replicas=1
kubectl autoscale deployment nginx-bench --cpu-percent=80 --min=1 --max=10
# After 24 hours, query the metrics
kubectl top pod -l app=nginx-bench --containers
# Use the P99 value for requests, P95 for limits
apiVersion: v1
kind: Pod
metadata:
name: optimized-app
spec:
containers:
- name: app
resources:
requests:
cpu: "800m" # Based on P99 observed usage
memory: "2Gi" # Based on steady-state + 20% buffer
limits:
cpu: "2000m" # 2.5x request for burst headroom
memory: "4Gi" # 2x request to prevent OOM
Best Practice: Run load tests that simulate production traffic patterns, not just peak throughput. Many applications show different resource profiles under sustained load vs. burst load. Use the Kubernetes metrics-server for CPU/memory and Prometheus for application-level metrics like request latency under load.
2. Node Placement Strategies — Keeping Critical Workloads Isolated
Mixing latency-sensitive services with batch jobs on the same node causes CPU contention and unpredictable performance. Use node groups with taints and tolerations to create dedicated compute pools.
# Create a managed node group for latency-sensitive workloads
# eksctl command
eksctl create nodegroup \
--cluster my-cluster \
--name api-priority \
--node-type m6i.xlarge \
--nodes 3 \
--nodes-min 3 \
--nodes-max 10 \
--node-labels "workload-type=latency-sensitive" \
--taints "CriticalAddonsOnly=true:NoSchedule"
# Pod toleration to schedule on the dedicated node group
apiVersion: apps/v1
kind: Deployment
metadata:
name: real-time-api
spec:
template:
spec:
tolerations:
- key: "CriticalAddonsOnly"
operator: "Equal"
value: "true"
effect: "NoSchedule"
nodeSelector:
workload-type: latency-sensitive
containers:
- name: api
resources:
requests:
cpu: "4"
memory: "8Gi"
Best Practice: Reserve at least 200 milliCPU and 256 Mi memory for system daemonsets (kube-proxy, CoreDNS, CSI drivers) using the kube-reserved and system-reserved kubelet flags in your node group launch template. Without this reservation, system pods compete directly with your application pods.
3. Container Runtime Performance — Kernel Tuning with Bottlerocket
Bottlerocket is a purpose-built Linux OS for containers, available as an EKS optimized AMI. It reduces OS overhead, boots faster, and runs with a smaller attack surface compared to general-purpose Amazon Linux 2.
# Launch a Bottlerocket node group with eksctl
eksctl create nodegroup \
--cluster my-cluster \
--name bottlerocket-nodes \
--ami-family Bottlerocket \
--instance-type m6i.large \
--nodes 4 \
--node-volume-size 50 \
--ssh-access \
--ssh-public-key ~/.ssh/id_rsa.pub
# Verify Bottlerocket nodes are ready
kubectl get nodes -l eks.amazonaws.com/compute-type=ec2,bottlerocket
Performance tuning via Bottlerocket's TOML configuration:
# /etc/bottlerocket.toml (applied via userdata)
[settings.kernel]
vm_max_map_count = "262144"
net_core_rmem_max = "16777216"
net_core_wmem_max = "16777216"
[settings.container-registry]
max-concurrent-uploads = "10"
[settings.aws]
enable-spot-instance-draining = true
Best Practice: Bottlerocket excels for workloads that are CPU-bound or require rapid scaling (its boot time is ~30 seconds vs. 60-90 seconds for Amazon Linux 2). Use it for your web tier and stateless services; keep Amazon Linux 2 for workloads that need kernel modules or legacy compatibility.
4. Monitoring and Observability Stack — Prometheus + Grafana
You cannot optimize what you cannot measure. A production-grade monitoring stack gives you real-time visibility into pod resource usage, node saturation, and application performance metrics.
# Deploy kube-prometheus-stack (includes Prometheus, Grafana, Alertmanager)
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--create-namespace \
--set prometheus.prometheusSpec.retention="30d" \
--set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage="100Gi" \
--set grafana.adminPassword="secure-initial-password" \
--set alertmanager.alertmanagerSpec.storage.volumeClaimTemplate.spec.resources.requests.storage="10Gi"
Critical performance dashboards to create in Grafana:
- Node Saturation: CPU, memory, disk I/O, and network throughput per node
- Pod Resource Usage: Actual usage vs. requests vs. limits for every deployment
- Scheduler Latency: Time from pod creation to successful scheduling
- API Server Latency: Response times for kubectl and controller operations
- Container Restart Rate: Spikes indicate OOM kills or crash loops
# Example PromQL queries for performance analysis
# Pods exceeding 80% of their CPU request (potential throttling candidates)
(sum(rate(container_cpu_usage_seconds_total{namespace="production"}[5m])) by (pod)
/
sum(kube_pod_container_resource_requests{resource="cpu",namespace="production"}) by (pod)) > 0.8
# Nodes with less than 15% allocatable memory (scheduling risk)
(sum(kube_node_status_allocatable{resource="memory"}) - sum(kube_node_status_capacity{resource="memory"}))
/ sum(kube_node_status_capacity{resource="memory"}) < 0.15
# API server request latency P99 (detect control plane overload)
histogram_quantile(0.99,
sum(rate(apiserver_request_duration_seconds_bucket[5m])) by (le, verb, resource))
Best Practice: Configure Alertmanager rules that fire before performance degrades — for example, when any node exceeds 85% CPU for 10 minutes or when the scheduler pending pod count exceeds 50. Preemptive alerts let you scale out before users notice slowdowns.
5. Load Balancing Optimization — AWS Load Balancer Controller
The AWS Load Balancer Controller replaces the legacy in-tree provider with native support for Application Load Balancers (ALB) and Network Load Balancers (NLB). It gives you fine-grained control over traffic routing, SSL termination, and target group health checks.
# Install the AWS Load Balancer Controller
helm repo add eks https://aws.github.io/eks-charts
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
--namespace kube-system \
--set clusterName=my-cluster \
--set serviceAccount.create=true \
--set serviceAccount.name=aws-load-balancer-controller \
--set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="arn:aws:iam::123456789012:role/AWSLoadBalancerControllerRole"
# Ingress with performance-tuned ALB settings
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: production-ingress
namespace: production
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/healthcheck-path: "/healthz"
alb.ingress.kubernetes.io/healthcheck-interval-seconds: "5"
alb.ingress.kubernetes.io/healthcheck-timeout-seconds: "3"
alb.ingress.kubernetes.io/healthy-threshold-count: "2"
alb.ingress.kubernetes.io/unhealthy-threshold-count: "2"
alb.ingress.kubernetes.io/target-group-attributes: |
deregistration_delay.timeout_seconds=30
slow_start.duration_seconds=0
load_balancing.algorithm.type=least_outstanding_requests
alb.ingress.kubernetes.io/ssl-policy: "ELBSecurityPolicy-TLS13-1-2-2021-06"
spec:
rules:
- host: api.example.com
http:
paths:
- path: /v2/*
pathType: Prefix
backend:
service:
name: api-v2-service
port:
number: 443
Best Practice: Use IP target type (target-type: ip) instead of instance mode to route traffic directly to pods, bypassing the kube-proxy hop. Enable least_outstanding_requests algorithm to prevent slow pods from receiving new requests. Set aggressive health checks (5-second intervals with 2-second timeout) to detect and eject failing pods within 10 seconds.
Part 4: Bringing It All Together — A Cohesive Optimization Workflow
Continuous Optimization Pipeline
The most successful EKS teams treat optimization as a recurring workflow, not a one-time project. Here's a battle-tested weekly cadence:
- Monday — Review cost metrics: Check EC2 and Fargate spend in AWS Cost Explorer. Identify underutilized nodes (CPU < 20% for 24+ hours) and rightsizing candidates.
- Wednesday — Security scan audit: Review image scan results from ECR Inspector and Trivy. Apply patches to images with HIGH or CRITICAL CVEs discovered in the past week.
- Friday — Performance regression check: Compare P99 latency percentiles against last week's baseline. Investigate any deployment whose P99 increased by >15%.
- Monthly — Full cluster review: Update IAM policies based on Access Analyzer findings. Rotate any secrets older than 90 days. Test cluster autoscaling behavior with a controlled load spike.
Automated Policy Enforcement Example
Use OPA Gatekeeper to codify your cost, security, and performance policies as automated constraints that reject non-compliant resources at admission time:
# Gatekeeper constraint: require resource requests on all production pods
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredResources
metadata:
name: production-resource-requirements
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaceSelector:
matchLabels:
environment: production
parameters:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "2000m"
memory: "4Gi"
---
# Gatekeeper constraint: block pods using the 'latest' image tag
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sBlockLatestTag
metadata:
name: block-latest-image-tag
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment", "StatefulSet"]
parameters:
excludedImages: ["kube-system/*"]
Best Practice: Run Gatekeeper in "dry-run" or "warn" mode for the first two weeks to discover violations without breaking deployments. Switch to "deny" only after the team has resolved all pre-existing violations. This prevents policy enforcement from becoming a blocker to legitimate releases.
Conclusion
Operating EKS clusters efficiently requires deliberate, continuous attention across three interconnected domains. Cost optimization is not just about spending less — it's about aligning infrastructure expenditure with actual workload demand through right-sizing, dynamic scaling, and intelligent scheduling. Security hardening is a layered responsibility where IAM roles, network isolation, pod security standards, and secrets management form a defense-in-depth strategy that protects both the control plane and your application data. Performance optimization is the ongoing discipline of measuring, tuning, and validating — ensuring that every millisecond of latency and every CPU cycle delivers value to your end users.
The code examples and configurations in this tutorial are designed to be production-ready starting points. Adapt them to your specific workload profiles, compliance requirements, and traffic patterns. Remember that the most effective optimization strategy is iterative: implement one practice per sprint, measure its impact, and then move to the next. An EKS cluster that is cost-efficient, security-hardened, and performance-tuned is not a destination — it's a continuously maintained state of operational excellence.