AWS EKS Add-Ons: VPC CNI, CoreDNS, kube-proxy and More
EKS add-ons are the operational backbone of every production Kubernetes cluster on AWS. They handle networking (VPC CNI), service discovery (CoreDNS), traffic rules (kube-proxy), persistent storage (EBS/EFS CSI), load balancing (AWS Load Balancer Controller), and even runtime security (GuardDuty). AWS manages the lifecycle of these components — patching security vulnerabilities, publishing new versions, and letting you upgrade with a single API call — but you still need to understand their internals to tune them correctly and avoid nasty surprises at scale. This guide goes deep on every major add-on, with real configuration, kubectl commands, YAML manifests, and Terraform.
Table of Contents
EKS Add-Ons Overview
An EKS add-on is a curated software package that AWS tests against each Kubernetes version and publishes with a clearly defined compatibility matrix. Before managed add-ons existed, operators had to manually apply the VPC CNI DaemonSet, CoreDNS Deployment, and kube-proxy DaemonSet, then carefully track which versions worked with which Kubernetes release. A missed update would leave a cluster running a CNI with a known CVE or a DNS server incompatible with new API objects.
Managed add-ons change this in three ways:
- Version lifecycle: AWS publishes new add-on versions within days of an upstream release and marks old versions deprecated. The EKS console shows a badge when an update is available.
- Conflict resolution policies: You control whether an update overwrites your custom configuration (
OVERWRITE) or preserves it (PRESERVE). A third option,NONE, fails the update if conflicts exist — useful for CI gates. - Configuration schema: Newer add-ons expose a JSON schema for
configuration_valuesso you can tune parameters (e.g., replica counts, environment variables) without patching the manifest directly.
Managed vs Self-Managed Add-Ons
Self-managed components (e.g., Helm-installed Cluster Autoscaler, Prometheus operator) are not tracked by the EKS add-ons API — you own every aspect of their upgrade path. Managed add-ons are the opposite: EKS tracks their version, reports health, and can auto-update them if you configure the --resolve-conflicts flag. The trade-off is that managed add-ons impose constraints on which fields you can customize without triggering a conflict warning.
aws eks describe-addon-versions --kubernetes-version 1.30 to see which add-on versions are available for the target Kubernetes version. You must update add-ons after the control plane upgrade, not before.
List all add-ons in a cluster and their current status:
# List installed add-ons
aws eks list-addons --cluster-name prod-cluster
# Describe a specific add-on
aws eks describe-addon \
--cluster-name prod-cluster \
--addon-name vpc-cni
# Check available versions for an add-on
aws eks describe-addon-versions \
--addon-name vpc-cni \
--kubernetes-version 1.30 \
--query 'addons[0].addonVersions[*].addonVersion'
Install an add-on that isn't yet present in your cluster:
aws eks create-addon \
--cluster-name prod-cluster \
--addon-name aws-ebs-csi-driver \
--addon-version v1.30.0-eksbuild.1 \
--service-account-role-arn arn:aws:iam::123456789012:role/AmazonEKS_EBS_CSI_DriverRole \
--resolve-conflicts OVERWRITE
VPC CNI (aws-node)
The Amazon VPC CNI plugin (aws-node DaemonSet in kube-system) assigns real VPC IP addresses to pods. Unlike overlay networks (Flannel, Weave), pods get native VPC IPs — they are routable from other VPCs, on-premises networks, and AWS services without NAT. This makes debugging much simpler and eliminates the performance overhead of encapsulation.
The CNI plugin manages Elastic Network Interfaces (ENIs) on each node. It pre-allocates a warm pool of IPs so that pod scheduling doesn't have to wait for ENI attachment. Two environment variables control this pool:
WARM_ENI_TARGET(default: 1) — keep this many spare ENIs attached and ready. Set to 0 on large nodes to reduce wasted IPs.WARM_IP_TARGET— keep this many spare IPs available across attached ENIs. More precise than WARM_ENI_TARGET for dense pods-per-node configurations.MINIMUM_IP_TARGET— floor for the IP pool, prevents aggressive ENI release during scale-down.
# Patch VPC CNI environment variables
kubectl set env daemonset aws-node \
-n kube-system \
WARM_ENI_TARGET=0 \
WARM_IP_TARGET=5 \
MINIMUM_IP_TARGET=10
# Verify the change
kubectl get daemonset aws-node -n kube-system -o yaml | \
grep -A 20 env:
Prefix Delegation — 16x More IPs per ENI
By default, each ENI slot holds one secondary IP. With prefix delegation (available on Nitro instances), each slot holds a /28 CIDR (16 IPs). This dramatically increases pod density on expensive instance types like m5.4xlarge.
# Enable prefix delegation
kubectl set env daemonset aws-node \
-n kube-system \
ENABLE_PREFIX_DELEGATION=true \
WARM_PREFIX_TARGET=1
# Verify pods per node increased
kubectl describe node <node-name> | grep "pods\|allocatable" -A 5
Custom Networking for More IPs
When your primary VPC CIDR is exhausted but you've added a secondary CIDR (e.g., 100.64.0.0/16), custom networking lets pods use IPs from that secondary CIDR while nodes keep their primary CIDR IPs:
# 1. Enable custom networking
kubectl set env daemonset aws-node \
-n kube-system \
AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG=true \
ENI_CONFIG_LABEL_DEF=topology.kubernetes.io/zone
# 2. Create an ENIConfig per AZ pointing to secondary subnets
apiVersion: crd.k8s.amazonaws.com/v1alpha1
kind: ENIConfig
metadata:
name: us-east-1a
spec:
subnet: subnet-0abc123def456789a # secondary subnet in us-east-1a
securityGroups:
- sg-0abc123def456789b
---
apiVersion: crd.k8s.amazonaws.com/v1alpha1
kind: ENIConfig
metadata:
name: us-east-1b
spec:
subnet: subnet-0abc123def456789c
securityGroups:
- sg-0abc123def456789b
IPv6 Support
EKS supports IPv6 clusters where pods receive IPv6 addresses from the VPC IPv6 CIDR. Enable this at cluster creation time — you cannot switch an existing IPv4 cluster to IPv6:
# eksctl config for IPv6 cluster
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: ipv6-cluster
region: us-east-1
version: "1.30"
kubernetesNetworkConfig:
ipFamily: IPv6
CoreDNS Configuration and Tuning
CoreDNS is the cluster DNS server, deployed as a Deployment (not a DaemonSet) in kube-system. Every pod's /etc/resolv.conf points to the CoreDNS ClusterIP. At scale, DNS becomes a bottleneck — hundreds of pods making lookup requests for every new connection creates a thundering herd on two CoreDNS replicas.
The Corefile
CoreDNS configuration lives in the coredns ConfigMap in kube-system. The default Corefile handles in-cluster names via the kubernetes plugin and forwards everything else to the VPC DNS resolver (169.254.169.253):
kubectl edit configmap coredns -n kube-system
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
Custom Domain Forwarding
If you need pods to resolve an on-premises domain (e.g., internal.corp) via a private DNS server, add a stub zone block:
.:53 {
# ... existing config ...
}
internal.corp:53 {
errors
cache 30
forward . 10.0.0.2 10.0.0.3 # on-premises DNS servers
}
Scaling CoreDNS for Large Clusters
The default 2 replicas are insufficient beyond ~500 nodes. Use the configuration_values JSON when managing via add-on API to set replica count and resource requests:
aws eks update-addon \
--cluster-name prod-cluster \
--addon-name coredns \
--configuration-values '{"replicaCount":4,"resources":{"requests":{"cpu":"100m","memory":"256Mi"},"limits":{"cpu":"200m","memory":"512Mi"}}}' \
--resolve-conflicts PRESERVE
NodeLocal DNSCache
NodeLocal DNSCache runs a DNS caching agent on every node using a link-local IP (169.254.20.10). Pods hit the local cache first, eliminating the network hop to CoreDNS for cached names. This reduces latency from ~1ms to ~0.1ms for cached lookups and dramatically reduces load on CoreDNS pods.
# Deploy NodeLocal DNSCache
# Replace CLUSTER_DNS with your CoreDNS ClusterIP
CLUSTER_DNS=$(kubectl get svc kube-dns -n kube-system -o jsonpath='{.spec.clusterIP}')
curl -s https://raw.githubusercontent.com/kubernetes/kubernetes/v1.30.0/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml \
| sed "s/__PILLAR__DNS__SERVER__/${CLUSTER_DNS}/g" \
| sed "s/__PILLAR__LOCAL__DNS__/169.254.20.10/g" \
| sed "s/__PILLAR__DNS__DOMAIN__/cluster.local/g" \
| kubectl apply -f -
# Verify the DaemonSet is running on all nodes
kubectl get daemonset node-local-dns -n kube-system
ndots:2 (instead of the default 5) in your pod's dnsConfig to reduce the number of search-domain retries for external names. With ndots:5, a lookup for api.example.com tries five fully-qualified permutations before going to the real DNS — that is 5x the queries.
spec:
dnsConfig:
options:
- name: ndots
value: "2"
- name: single-request-reopen
kube-proxy: iptables vs ipvs
kube-proxy is a DaemonSet that maintains network rules on each node so that Kubernetes Service ClusterIPs route to the correct pod endpoints. It watches the API server for Service and Endpoint changes and translates them into either iptables rules or IPVS rules depending on mode.
iptables Mode (default)
In iptables mode, kube-proxy appends DNAT rules to iptables chains. Traffic to a ClusterIP hits the KUBE-SVC chain, which randomly selects one of the endpoint DNAT rules. This is simple and battle-tested, but has O(n) lookup time — with 10,000 services, every packet traverses potentially thousands of rules before matching. At large scale you start to see measurable CPU overhead in the kernel.
ipvs Mode
IPVS (IP Virtual Server) uses a hash table for service lookups — O(1) regardless of the number of services. It also supports richer load-balancing algorithms (round-robin, least-connections, destination hashing) compared to iptables' random selection.
# Switch kube-proxy to ipvs mode via EKS add-on configuration_values
aws eks update-addon \
--cluster-name prod-cluster \
--addon-name kube-proxy \
--configuration-values '{"mode":"ipvs"}' \
--resolve-conflicts OVERWRITE
# Verify mode on a node
kubectl exec -n kube-system $(kubectl get pod -n kube-system -l k8s-app=kube-proxy -o name | head -1) \
-- kube-proxy --version
# Check ipvs rules
kubectl exec -n kube-system $(kubectl get pod -n kube-system -l k8s-app=kube-proxy -o name | head -1) \
-- ipvsadm -L -n | head -30
conntrack Tuning
Under high connection rates (e.g., short-lived HTTP/2 or gRPC connections at thousands per second), the conntrack table can fill up, causing nf_conntrack: table full, dropping packet errors. Tune the limits at the node level via a DaemonSet that sets sysctl values:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: sysctl-tuner
namespace: kube-system
spec:
selector:
matchLabels:
app: sysctl-tuner
template:
metadata:
labels:
app: sysctl-tuner
spec:
hostNetwork: true
hostPID: true
tolerations:
- operator: Exists
initContainers:
- name: tuner
image: busybox:1.36
securityContext:
privileged: true
command:
- sh
- -c
- |
sysctl -w net.netfilter.nf_conntrack_max=1048576
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=86400
sysctl -w net.core.somaxconn=65535
containers:
- name: pause
image: k8s.gcr.io/pause:3.9
kubeProxyReplacement: true). Cilium bypasses iptables/ipvs entirely, hooks into the kernel at the socket level, and adds Hubble observability. This is a significant operational change but delivers 2-3x better connection throughput at scale.
EBS CSI Driver
The Amazon EBS CSI Driver replaces the deprecated in-tree EBS provisioner (removed in Kubernetes 1.27). It runs as a Deployment (controller) plus DaemonSet (node plugin) and requires an IAM role for the service account to make EBS API calls.
Prerequisites: IRSA Setup
# Create the IAM service account
eksctl create iamserviceaccount \
--name ebs-csi-controller-sa \
--namespace kube-system \
--cluster prod-cluster \
--attach-policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy \
--approve \
--role-only \
--role-name AmazonEKS_EBS_CSI_DriverRole
# Install the add-on
aws eks create-addon \
--cluster-name prod-cluster \
--addon-name aws-ebs-csi-driver \
--service-account-role-arn arn:aws:iam::123456789012:role/AmazonEKS_EBS_CSI_DriverRole
gp3 StorageClass
gp3 is the preferred volume type — 20% cheaper than gp2 and you can independently configure IOPS and throughput without over-provisioning size:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gp3
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
parameters:
type: gp3
iops: "6000"
throughput: "250"
encrypted: "true"
kmsKeyId: arn:aws:kms:us-east-1:123456789012:key/mrk-abc123
PVC Example and Cross-AZ Considerations
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
namespace: production
spec:
accessModes:
- ReadWriteOnce
storageClassName: gp3
resources:
requests:
storage: 100Gi
volumeBindingMode: WaitForFirstConsumer (as in the gp3 StorageClass above) so the PVC is provisioned in the same AZ as the pod that claims it. Never use Immediate binding with EBS.
Volume Snapshots
# Install VolumeSnapshot CRDs (required separately)
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/v7.0.1/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/v7.0.1/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/v7.0.1/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml
# Create a VolumeSnapshotClass
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: ebs-vsc
driver: ebs.csi.aws.com
deletionPolicy: Retain
# Take a snapshot
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: postgres-snap-20260609
spec:
volumeSnapshotClassName: ebs-vsc
source:
persistentVolumeClaimName: postgres-data
EFS CSI Driver
Amazon EFS provides a fully managed NFS filesystem that can be simultaneously mounted by pods across multiple AZs and multiple nodes — something EBS (ReadWriteOnce) cannot do. The EFS CSI Driver supports both static provisioning (you create the EFS filesystem manually) and dynamic provisioning (the driver creates access points on demand).
Installing the Add-On
# Create IRSA
eksctl create iamserviceaccount \
--name efs-csi-controller-sa \
--namespace kube-system \
--cluster prod-cluster \
--attach-policy-arn arn:aws:iam::aws:policy/service-role/AmazonEFSCSIDriverPolicy \
--approve \
--role-name AmazonEKS_EFS_CSI_DriverRole
aws eks create-addon \
--cluster-name prod-cluster \
--addon-name aws-efs-csi-driver \
--service-account-role-arn arn:aws:iam::123456789012:role/AmazonEKS_EFS_CSI_DriverRole
Dynamic Provisioning with Access Points
Access points enforce a specific POSIX user identity and a root directory path, making them ideal for multi-tenant scenarios where each namespace or team gets isolated storage on the same EFS filesystem:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: efs-sc
provisioner: efs.csi.aws.com
parameters:
provisioningMode: efs-ap # dynamic access point per PVC
fileSystemId: fs-0abc12345def67890
directoryPerms: "700"
gidRangeStart: "1000"
gidRangeEnd: "2000"
basePath: "/dynamic"
uid: "1000"
gid: "1000"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: shared-logs
namespace: team-a
spec:
accessModes:
- ReadWriteMany
storageClassName: efs-sc
resources:
requests:
storage: 50Gi
Static Provisioning
apiVersion: v1
kind: PersistentVolume
metadata:
name: efs-pv
spec:
capacity:
storage: 100Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: efs-sc
csi:
driver: efs.csi.aws.com
volumeHandle: fs-0abc12345def67890::fsap-0xyz123
AWS Load Balancer Controller
The AWS Load Balancer Controller is a Kubernetes controller that provisions Application Load Balancers (ALB) from Ingress objects and Network Load Balancers (NLB) from Service objects of type LoadBalancer. It replaced the older ALB Ingress Controller and the in-tree cloud provider load balancer logic.
Installation
eksctl create iamserviceaccount \
--cluster prod-cluster \
--namespace kube-system \
--name aws-load-balancer-controller \
--attach-policy-arn arn:aws:iam::123456789012:policy/AWSLoadBalancerControllerIAMPolicy \
--approve
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName=prod-cluster \
--set serviceAccount.create=false \
--set serviceAccount.name=aws-load-balancer-controller \
--set enableShield=false \
--set enableWaf=false \
--set enableWafv2=false
IngressClass Configuration
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: alb
annotations:
ingressclass.kubernetes.io/is-default-class: "true"
spec:
controller: ingress.k8s.aws/alb
ALB Ingress with HTTPS and WAF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-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/listen-ports: '[{"HTTP":80},{"HTTPS":443}]'
alb.ingress.kubernetes.io/ssl-redirect: "443"
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:123456789012:certificate/abc-def
alb.ingress.kubernetes.io/healthcheck-path: /healthz
alb.ingress.kubernetes.io/healthcheck-interval-seconds: "15"
alb.ingress.kubernetes.io/success-codes: "200,204"
alb.ingress.kubernetes.io/wafv2-acl-arn: arn:aws:wafv2:us-east-1:123456789012:regional/webacl/prod-waf/abc
alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=60
alb.ingress.kubernetes.io/group.name: prod-alb # share one ALB across Ingresses
spec:
rules:
- host: api.example.com
http:
paths:
- path: /v1
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8080
NLB for TCP/UDP
apiVersion: v1
kind: Service
metadata:
name: grpc-service
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: external
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
spec:
type: LoadBalancer
ports:
- port: 443
targetPort: 9090
protocol: TCP
selector:
app: grpc-service
TargetGroupBinding for External Targets
TargetGroupBinding lets you register non-Kubernetes targets (e.g., Lambda functions, on-premises servers) into an ALB target group while still managing routing via Kubernetes Ingress:
apiVersion: elbv2.k8s.aws/v1beta1
kind: TargetGroupBinding
metadata:
name: legacy-api-tgb
spec:
serviceRef:
name: legacy-api-proxy
port: 80
targetGroupARN: arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/legacy-api/abcdef
GuardDuty EKS Add-On
Amazon GuardDuty's EKS Runtime Monitoring feature deploys a security agent (the aws-guardduty-agent DaemonSet) onto every node. The agent uses eBPF to capture syscall events — process spawning, network connections, file operations — and streams them to GuardDuty for analysis. This provides runtime threat detection without requiring application changes.
Enabling the Add-On
# Enable EKS Runtime Monitoring in GuardDuty
aws guardduty update-detector \
--detector-id $(aws guardduty list-detectors --query 'DetectorIds[0]' --output text) \
--features '[{"Name":"EKS_RUNTIME_MONITORING","Status":"ENABLED","AdditionalConfiguration":[{"Name":"EKS_ADDON_MANAGEMENT","Status":"ENABLED"}]}]'
# Verify the DaemonSet was deployed
kubectl get daemonset aws-guardduty-agent -n amazon-guardduty
kubectl get pods -n amazon-guardduty
Container-Specific Threat Findings
GuardDuty produces EKS-specific finding types that are distinct from EC2 findings:
- CryptoCurrency:Runtime/BitcoinTool.B!DNS — a container queried a known cryptocurrency mining domain
- Execution:Runtime/NewBinaryExecuted — a binary that was not in the original container image was executed (classic sign of container breakout or RCE)
- PrivilegeEscalation:Runtime/CGroupsReleaseAgentFileModified — a pod attempted to write to the cgroup release_agent (a known container escape vector)
- Discovery:Runtime/MaliciousFile — a process opened a file matching a known malware signature
- Backdoor:Runtime/C&CActivity.B — a container established a connection to a known command-and-control server
guardduty.amazonaws.com/agent-exclusion: "true" to the namespace. This prevents false positives from legitimate load testing.
# Exclude a namespace from monitoring
kubectl label namespace load-testing \
guardduty.amazonaws.com/agent-exclusion=true
# Check GuardDuty findings via CLI
aws guardduty list-findings \
--detector-id $(aws guardduty list-detectors --query 'DetectorIds[0]' --output text) \
--finding-criteria '{"Criterion":{"resource.resourceType":{"Equals":["EKSCluster"]}}}' \
--query 'FindingIds'
Managing Add-Ons with Terraform
The aws_eks_addon Terraform resource manages the full lifecycle of EKS add-ons declaratively. Version pinning, configuration values, and IRSA associations are all expressed in HCL, making cluster state reproducible and auditable in version control.
# variables.tf
variable "cluster_name" {
default = "prod-cluster"
}
variable "cluster_version" {
default = "1.30"
}
# data sources
data "aws_eks_addon_version" "vpc_cni" {
addon_name = "vpc-cni"
kubernetes_version = var.cluster_version
most_recent = true
}
data "aws_eks_addon_version" "coredns" {
addon_name = "coredns"
kubernetes_version = var.cluster_version
most_recent = true
}
# add-ons.tf
resource "aws_eks_addon" "vpc_cni" {
cluster_name = var.cluster_name
addon_name = "vpc-cni"
addon_version = data.aws_eks_addon_version.vpc_cni.version
resolve_conflicts_on_update = "PRESERVE"
service_account_role_arn = aws_iam_role.vpc_cni.arn
configuration_values = jsonencode({
env = {
ENABLE_PREFIX_DELEGATION = "true"
WARM_PREFIX_TARGET = "1"
}
})
tags = {
Environment = "prod"
ManagedBy = "terraform"
}
}
resource "aws_eks_addon" "coredns" {
cluster_name = var.cluster_name
addon_name = "coredns"
addon_version = data.aws_eks_addon_version.coredns.version
resolve_conflicts_on_update = "PRESERVE"
configuration_values = jsonencode({
replicaCount = 4
resources = {
requests = { cpu = "100m", memory = "256Mi" }
limits = { cpu = "200m", memory = "512Mi" }
}
})
}
resource "aws_eks_addon" "kube_proxy" {
cluster_name = var.cluster_name
addon_name = "kube-proxy"
resolve_conflicts_on_update = "OVERWRITE"
}
resource "aws_eks_addon" "ebs_csi_driver" {
cluster_name = var.cluster_name
addon_name = "aws-ebs-csi-driver"
service_account_role_arn = aws_iam_role.ebs_csi.arn
resolve_conflicts_on_update = "OVERWRITE"
}
resource "aws_eks_addon" "efs_csi_driver" {
cluster_name = var.cluster_name
addon_name = "aws-efs-csi-driver"
service_account_role_arn = aws_iam_role.efs_csi.arn
resolve_conflicts_on_update = "OVERWRITE"
}
IAM Role for EBS CSI (IRSA)
data "aws_iam_policy_document" "ebs_csi_assume" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.eks.arn]
}
condition {
test = "StringEquals"
variable = "${replace(aws_iam_openid_connect_provider.eks.url, "https://", "")}:sub"
values = ["system:serviceaccount:kube-system:ebs-csi-controller-sa"]
}
}
}
resource "aws_iam_role" "ebs_csi" {
name = "AmazonEKS_EBS_CSI_DriverRole"
assume_role_policy = data.aws_iam_policy_document.ebs_csi_assume.json
}
resource "aws_iam_role_policy_attachment" "ebs_csi" {
role = aws_iam_role.ebs_csi.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy"
}
most_recent = true in aws_eks_addon_version data sources only in development. In production, pin to a specific version (e.g., v1.30.0-eksbuild.1) and update it deliberately after testing in a staging cluster. This prevents automatic version bumps on a terraform apply from introducing unexpected behaviour.
Add-On Update Strategy
Updating add-ons is a two-phase process: control plane upgrade first, add-on updates second. Never upgrade add-ons before the control plane — some add-on versions require newer API server features that only exist in the newer Kubernetes version. Always upgrade add-ons before upgrading node groups, because the new kubelet on updated nodes may require newer CNI or kube-proxy versions to schedule pods correctly.
Conflict Resolution Policies
- OVERWRITE: AWS replaces any field in the add-on's managed objects that differs from the desired add-on configuration. Use this for add-ons where you don't customise the managed DaemonSet/Deployment directly.
- PRESERVE: AWS skips fields that have been manually modified. Your custom resource requests, replica counts, or environment variables are retained. Use this when you've tuned VPC CNI env vars or CoreDNS replicas.
- NONE: The update fails if any conflict is detected. Useful in CI/CD pipelines to enforce that no manual edits have drifted from the managed configuration before proceeding.
# Safe add-on update sequence after a cluster version upgrade
# 1. Check current add-on versions
aws eks list-addons --cluster-name prod-cluster | jq '.addons[]' | \
xargs -I{} aws eks describe-addon \
--cluster-name prod-cluster \
--addon-name {} \
--query '{name: addon.addonName, version: addon.addonVersion, status: addon.status}'
# 2. Find the default version for the new k8s version
aws eks describe-addon-versions \
--kubernetes-version 1.30 \
--query 'addons[*].{name:addonName, default:addonVersions[?compatibilities[?defaultVersion==`true`]].addonVersion}' \
--output table
# 3. Update each add-on
for ADDON in vpc-cni coredns kube-proxy aws-ebs-csi-driver; do
echo "Updating $ADDON..."
aws eks update-addon \
--cluster-name prod-cluster \
--addon-name $ADDON \
--resolve-conflicts PRESERVE
# Wait for the update to complete before moving to the next
aws eks wait addon-active \
--cluster-name prod-cluster \
--addon-name $ADDON
echo "$ADDON is active"
done
Testing Updates in Staging First
# Compare add-on versions between staging and production
diff \
<(aws eks list-addons --cluster-name staging-cluster --output text | sort) \
<(aws eks list-addons --cluster-name prod-cluster --output text | sort)
# Run a quick smoke test after add-on update
kubectl run dns-test --image=busybox:1.36 --rm -it --restart=Never -- \
nslookup kubernetes.default.svc.cluster.local
kubectl run pvc-test --image=busybox:1.36 --rm -it --restart=Never \
--overrides='{"spec":{"volumes":[{"name":"data","persistentVolumeClaim":{"claimName":"test-pvc"}}],"containers":[{"name":"c","image":"busybox:1.36","command":["sh","-c","echo OK > /data/test && cat /data/test"],"volumeMounts":[{"mountPath":"/data","name":"data"}]}]}}' \
-- true
Rollback
If an add-on update causes problems, roll back by specifying the previous version explicitly:
# Roll back VPC CNI to previous version
aws eks update-addon \
--cluster-name prod-cluster \
--addon-name vpc-cni \
--addon-version v1.18.0-eksbuild.1 \
--resolve-conflicts OVERWRITE
# Monitor the DaemonSet rollout
kubectl rollout status daemonset aws-node -n kube-system --timeout=5m
configuration_values. When you tune WARM_IP_TARGET or CoreDNS replicas, record the change in your IaC repo immediately. Drift between the running state and your Terraform causes update conflicts and makes rollbacks harder to reason about.