AWS EKS Fargate: Serverless Kubernetes Workloads
Amazon EKS Fargate lets you run Kubernetes pods without provisioning or managing EC2 nodes. AWS handles the underlying compute — you declare a pod, define a Fargate profile that matches it, and the scheduler places it on invisible infrastructure. No AMIs to patch, no node groups to right-size, no SSH access to accidentally leave open. Each pod runs in its own isolated microVM, giving you strong workload isolation at the unit of the pod rather than the node. This guide covers everything you need to move real workloads onto EKS Fargate: profile configuration, networking deep-dive, storage workarounds, ALB Ingress setup, CoreDNS patching, Fluent Bit logging, and a line-item cost model.
Table of Contents
- EKS Fargate Overview
- Fargate Profiles and Pod Selectors
- Setting Up an EKS Cluster with Fargate
- Fargate vs Managed Node Groups
- Networking: VPC CNI and Per-Pod ENIs
- Storage Limitations and EFS Integration
- ALB Ingress Controller on Fargate
- CoreDNS on Fargate
- Logging with Fluent Bit Sidecar
- Cost Calculation and Optimization
- Frequently Asked Questions
EKS Fargate Overview
Traditional EKS requires you to manage a fleet of EC2 worker nodes: choose instance types, configure launch templates, patch AMIs, and handle node scaling. Fargate removes this entirely. When a pod matches a Fargate profile, the EKS control plane hands the pod to AWS Fargate, which provisions a dedicated microVM sized to the pod's resource requests, schedules the containers inside it, and terminates the VM when the pod exits.
Key characteristics of EKS Fargate:
- No node management: No EC2 worker nodes, no node groups, no SSH keys for nodes. The data plane is fully managed.
- Pod-level isolation: Every Fargate pod runs in its own Firecracker microVM. There is no shared kernel between pods, unlike EC2 nodes where multiple pods share the host OS.
- Per-pod billing: You pay for the vCPU and memory allocated to each pod, rounded up to the nearest configured size, billed per second with a minimum of 1 minute.
- Kubernetes-native: Pods are still Kubernetes pods. You use the same Deployments, Services, HPA, and RBAC you already know. Fargate is a scheduling backend, not a different API.
- Limitations to know upfront: No DaemonSets, no privileged containers, no hostPath volumes, no GPU instances, and limited to pods in specific namespaces/labels defined by profiles.
Fargate Profiles and Pod Selectors
A Fargate profile is the EKS resource that defines which pods should run on Fargate. It contains one or more selectors, each with a required namespace and optional label key-value pairs. A pod must match at least one selector — namespace is mandatory, labels are additive filters within that namespace.
Matching rules:
- If a pod's namespace matches the profile's namespace AND all specified labels match, the pod runs on Fargate.
- If no profile matches, the pod runs on EC2 nodes (or stays pending if no nodes are available).
- Multiple profiles can exist per cluster. If a pod matches multiple profiles, EKS uses the one with the most specific (longest) matching selector.
- Fargate profiles are associated with subnets — the pod's ENI is placed in one of those subnets. Use private subnets here.
Common selector patterns:
namespace: kube-system— required to run CoreDNS on Fargatenamespace: default— catch-all for default workloadsnamespace: production, labels: {tier: backend}— targeted profile for specific service tiernamespace: jobs— batch job namespace, all pods go to Fargate
Setting Up an EKS Cluster with Fargate
The fastest way to create an EKS cluster with Fargate is eksctl. The YAML below creates a cluster with no managed node groups and two Fargate profiles: one for kube-system (needed for CoreDNS) and one for a production namespace.
# cluster-fargate.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: my-fargate-cluster
region: us-east-1
version: "1.29"
# No managedNodeGroups block — pure Fargate cluster
fargateProfiles:
- name: fp-kube-system
selectors:
- namespace: kube-system
- name: fp-production
selectors:
- namespace: production
- namespace: production
labels:
tier: backend
- name: fp-jobs
selectors:
- namespace: jobs
# IAM OIDC provider — required for pod-level IAM roles (IRSA)
iam:
withOIDC: true
# VPC config — Fargate pods land in private subnets
vpc:
nat:
gateway: HighlyAvailable
Create the cluster:
# Create cluster from config file (takes ~15 minutes)
eksctl create cluster -f cluster-fargate.yaml
# Verify Fargate profiles
eksctl get fargateprofile --cluster my-fargate-cluster
# Check that CoreDNS pods ended up on Fargate
kubectl get pods -n kube-system -o wide
# FARGATE nodes appear as fargate-ip-X-X-X-X.ec2.internal in the NODE column
You can also create a Fargate profile for an existing cluster using the AWS CLI:
# Create a Fargate profile via CLI
aws eks create-fargate-profile \
--cluster-name my-fargate-cluster \
--fargate-profile-name fp-staging \
--pod-execution-role-arn arn:aws:iam::123456789012:role/AmazonEKSFargatePodExecutionRole \
--subnets subnet-0abc1234 subnet-0def5678 \
--selectors '[{"namespace":"staging"},{"namespace":"staging","labels":{"env":"preview"}}]'
# Wait for profile to become ACTIVE
aws eks describe-fargate-profile \
--cluster-name my-fargate-cluster \
--fargate-profile-name fp-staging \
--query 'fargateProfile.status'
Pod Execution Role
Fargate needs an IAM role to pull container images, write logs, and interact with VPC resources. This is the pod execution role — distinct from the pod's own service account IAM role (IRSA). eksctl creates it automatically; for manual setups, attach the AmazonEKSFargatePodExecutionRolePolicy managed policy.
# Create pod execution role manually
aws iam create-role \
--role-name AmazonEKSFargatePodExecutionRole \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "eks-fargate-pods.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
aws iam attach-role-policy \
--role-name AmazonEKSFargatePodExecutionRole \
--policy-arn arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy
Fargate vs Managed Node Groups
Neither option is universally better. The right choice depends on your workload profile, operational preferences, and cost tolerance.
| Dimension | EKS Fargate | Managed Node Groups (EC2) |
|---|---|---|
| Node management | None — AWS manages all compute | AWS manages AMI updates and node lifecycle |
| Pod isolation | Microvm per pod (Firecracker) | Shared kernel across pods on same node |
| DaemonSets | Not supported | Fully supported |
| GPU workloads | Not supported | Supported (g4dn, p3, p4d families) |
| Privileged containers | Not supported | Supported |
| hostPath / hostNetwork | Not supported | Supported |
| Persistent storage | EFS only (no EBS) | EBS, EFS, NVMe instance storage |
| Startup time | ~30–60s cold start per pod | ~5–15s (node pre-warmed) |
| Billing unit | vCPU-seconds + GB-seconds per pod | EC2 instance hours (node capacity) |
| Cost at low utilization | Lower — pay only for running pods | Higher — paying for idle node capacity |
| Cost at high utilization | Higher — no bin-packing discount | Lower — many pods share one large node |
| Max pod size | 4 vCPU / 30 GB RAM (configurable) | Limited only by node instance type |
| Best use case | Microservices, batch jobs, event-driven, staging environments | High-throughput services, stateful apps, GPU inference, custom OS config |
Networking: VPC CNI and Per-Pod ENIs
EKS Fargate uses the Amazon VPC CNI plugin, the same plugin used on EC2 nodes, but with one critical difference: instead of sharing an ENI across multiple pods on a node, each Fargate pod gets its own dedicated ENI attached directly to your VPC subnet.
Implications of per-pod ENIs:
- Native VPC IP addresses: Every Fargate pod gets a real VPC IP, meaning you can apply VPC-level routing, ACLs, and routing tables to individual pods.
- Security Groups for Pods: Fargate supports the EKS Security Groups for Pods feature natively. You can attach a security group directly to a pod via the
SecurityGroupPolicyCRD, giving pods their own firewall rules independent of the node security group. - Subnet capacity planning: Each Fargate pod consumes one ENI slot and one IP address from the subnet. For large Fargate workloads, ensure your private subnets have enough IP space (a /20 gives 4091 usable IPs).
- Pod-to-pod traffic stays in VPC: No overlay networking. Latency is equivalent to EC2-to-EC2 within the same AZ.
Assigning a Security Group to a Fargate pod:
# security-group-policy.yaml
apiVersion: vpcresources.k8s.aws/v1beta1
kind: SecurityGroupPolicy
metadata:
name: my-app-sgp
namespace: production
spec:
podSelector:
matchLabels:
app: payment-service
securityGroups:
groupIds:
- sg-0a1b2c3d4e5f6a7b8 # dedicated SG for payment-service pods
Apply with kubectl apply -f security-group-policy.yaml. All pods in the production namespace with the label app: payment-service will have that security group attached to their ENI, in addition to the cluster security group.
Storage Limitations and EFS Integration
EKS Fargate has significant storage constraints compared to EC2 nodes:
- No EBS volumes: EBS volumes attach to EC2 instances, not to microVMs. Fargate pods cannot use the EBS CSI driver.
- No hostPath: There is no host node filesystem to mount.
- No DaemonSets: The EBS CSI node daemonset cannot run on Fargate.
- Ephemeral storage: Each Fargate pod gets 20 GB of ephemeral storage by default (configurable up to 175 GB via
ephemeral-storageresource request). - Amazon EFS: The only supported persistent storage option. EFS is a managed NFS file system that mounts over the network, independent of the compute substrate.
Setting up EFS persistent storage for Fargate:
# 1. Install EFS CSI driver (Fargate-compatible — runs as a deployment, not DaemonSet)
helm repo add aws-efs-csi-driver https://kubernetes-sigs.github.io/aws-efs-csi-driver/
helm upgrade --install aws-efs-csi-driver \
aws-efs-csi-driver/aws-efs-csi-driver \
--namespace kube-system \
--set controller.serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=\
arn:aws:iam::123456789012:role/AmazonEKS_EFS_CSI_DriverRole
# 2. Create StorageClass for EFS
# efs-storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: efs-sc
provisioner: efs.csi.aws.com
parameters:
provisioningMode: efs-ap # uses EFS Access Points for isolation
fileSystemId: fs-0abc12345678def0 # your EFS filesystem ID
directoryPerms: "700"
gidRangeStart: "1000"
gidRangeEnd: "2000"
basePath: "/dynamic_provisioning"
---
# 3. PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data-pvc
namespace: production
spec:
accessModes:
- ReadWriteMany # EFS supports concurrent access from multiple pods
storageClassName: efs-sc
resources:
requests:
storage: 5Gi
---
# 4. Pod using the PVC
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-with-efs
namespace: production
spec:
replicas: 2
selector:
matchLabels:
app: app-with-efs
template:
metadata:
labels:
app: app-with-efs
spec:
containers:
- name: app
image: nginx:1.25
volumeMounts:
- name: data
mountPath: /var/app/data
resources:
requests:
cpu: "0.25"
memory: "512Mi"
limits:
cpu: "0.25"
memory: "512Mi"
volumes:
- name: data
persistentVolumeClaim:
claimName: app-data-pvc
provisioningMode: efs-ap, the EFS CSI driver automatically creates an EFS Access Point for each PVC. Access Points provide directory-level isolation — each PVC gets its own directory root with controlled POSIX permissions. This is the recommended approach for multi-tenant or multi-namespace EFS usage on Fargate.
ALB Ingress Controller on Fargate
Fargate pods cannot use classic LoadBalancer type Services backed by CLB or NLB pass-through in the same way — more importantly, the AWS Load Balancer Controller (which manages Application Load Balancers) can run on Fargate and route traffic to Fargate pods via IP target mode.
Install AWS Load Balancer Controller
# Step 1: Create IAM policy for the controller
curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/main/docs/install/iam_policy.json
aws iam create-policy \
--policy-name AWSLoadBalancerControllerIAMPolicy \
--policy-document file://iam_policy.json
# Step 2: Create service account with IRSA
eksctl create iamserviceaccount \
--cluster my-fargate-cluster \
--namespace kube-system \
--name aws-load-balancer-controller \
--attach-policy-arn arn:aws:iam::123456789012:policy/AWSLoadBalancerControllerIAMPolicy \
--approve
# Step 3: Install via Helm
helm repo add eks https://aws.github.io/eks-charts
helm upgrade --install aws-load-balancer-controller eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName=my-fargate-cluster \
--set serviceAccount.create=false \
--set serviceAccount.name=aws-load-balancer-controller \
--set region=us-east-1 \
--set vpcId=vpc-0abc123456789def0
Create an Ingress resource targeting Fargate pods. The critical annotation is target-type: ip — this tells the ALB to route directly to pod IPs rather than node IPs (which don't exist for Fargate):
# alb-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
namespace: production
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip # REQUIRED for Fargate
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80,"HTTPS":443}]'
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:123456789012:certificate/abc-def
alb.ingress.kubernetes.io/ssl-redirect: "443"
alb.ingress.kubernetes.io/healthcheck-path: /health
alb.ingress.kubernetes.io/healthcheck-interval-seconds: "15"
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app-service
port:
number: 8080
kube-system namespace has a Fargate profile, and verify the controller pods are scheduled and running before creating Ingress resources.
CoreDNS on Fargate
By default, CoreDNS deploys as a Deployment in the kube-system namespace on EC2 nodes. To run CoreDNS on Fargate, you need to:
- Create a Fargate profile for the
kube-systemnamespace (already covered in the cluster setup above). - Patch the CoreDNS deployment to remove the
eks.amazonaws.com/compute-type: ec2annotation that pins it to EC2 nodes.
# Patch CoreDNS to run on Fargate
# Remove the EC2 compute-type annotation from the CoreDNS pods
kubectl patch deployment coredns \
-n kube-system \
--type json \
-p='[{"op":"remove","path":"/spec/template/metadata/annotations/eks.amazonaws.com~1compute-type"}]'
# Restart CoreDNS pods so they get rescheduled to Fargate
kubectl rollout restart deployment coredns -n kube-system
# Verify — CoreDNS pods should now show a Fargate node in the NODE column
kubectl get pods -n kube-system -l k8s-app=kube-dns -o wide
After patching, CoreDNS pods will be evicted and rescheduled by the Fargate scheduler. There will be a brief DNS resolution gap (usually under 60 seconds) as the new Fargate-backed pods come up. Plan this during low-traffic periods or ensure you have enough replicas (default is 2) so at least one pod is always serving.
Logging with Fluent Bit Sidecar
On EC2 nodes, you can run the CloudWatch agent or Fluent Bit as a DaemonSet that tails logs from the node's /var/log/containers directory. On Fargate this is impossible — there is no node filesystem and no DaemonSet support. The two alternatives are:
- Native Fargate logging (AWS Fluent Bit built-in): Available via the
aws-fargate-loggingConfigMap — the simplest option for sending stdout/stderr to CloudWatch, Kinesis, or Firehose. - Fluent Bit sidecar: Full control over log routing, filtering, and enrichment.
Option 1: Native Fargate Logging (Recommended for Simple Cases)
# Create the aws-observability namespace and ConfigMap
# This activates the built-in Fluent Bit for all Fargate pods in the cluster
apiVersion: v1
kind: Namespace
metadata:
name: aws-observability
labels:
aws-observability: enabled
---
apiVersion: v1
kind: ConfigMap
metadata:
name: aws-logging
namespace: aws-observability
data:
output.conf: |
[OUTPUT]
Name cloudwatch_logs
Match *
region us-east-1
log_group_name /eks/my-fargate-cluster/app-logs
log_stream_prefix fargate-
auto_create_group true
filters.conf: |
[FILTER]
Name parser
Match *
Key_Name log
Parser json
Reserve_Data true
Option 2: Fluent Bit Sidecar for Advanced Routing
# fluent-bit-sidecar.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-with-logging
namespace: production
spec:
replicas: 2
selector:
matchLabels:
app: app-with-logging
template:
metadata:
labels:
app: app-with-logging
spec:
# Shared volume for log files between app and Fluent Bit sidecar
volumes:
- name: app-logs
emptyDir: {}
- name: fluent-bit-config
configMap:
name: fluent-bit-config
containers:
# Main application container
- name: app
image: my-app:1.0.0
resources:
requests:
cpu: "0.5"
memory: "1Gi"
limits:
cpu: "0.5"
memory: "1Gi"
volumeMounts:
- name: app-logs
mountPath: /var/log/app
# Fluent Bit sidecar
- name: fluent-bit
image: public.ecr.aws/aws-observability/aws-for-fluent-bit:latest
resources:
requests:
cpu: "0.1"
memory: "128Mi"
limits:
cpu: "0.1"
memory: "128Mi"
volumeMounts:
- name: app-logs
mountPath: /var/log/app
- name: fluent-bit-config
mountPath: /fluent-bit/etc/
---
apiVersion: v1
kind: ConfigMap
metadata:
name: fluent-bit-config
namespace: production
data:
fluent-bit.conf: |
[SERVICE]
Flush 5
Log_Level info
Parsers_File parsers.conf
[INPUT]
Name tail
Path /var/log/app/*.log
Tag app.*
Refresh_Interval 5
Mem_Buf_Limit 5MB
[FILTER]
Name record_modifier
Match app.*
Record cluster my-fargate-cluster
Record namespace ${NAMESPACE}
Record pod_name ${POD_NAME}
[OUTPUT]
Name cloudwatch_logs
Match app.*
region us-east-1
log_group_name /eks/my-fargate-cluster/app-logs
log_stream_name $(pod_name)
auto_create_group true
parsers.conf: |
[PARSER]
Name json
Format json
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L
Cost Calculation and Optimization
Fargate pricing has two dimensions billed per second (with a 1-minute minimum):
- vCPU: $0.04048 per vCPU-hour (us-east-1, 2026)
- Memory: $0.004445 per GB-hour (us-east-1, 2026)
Fargate rounds up pod resource requests to the nearest supported size combination. The available vCPU and memory pairings are:
- 0.25 vCPU: 0.5, 1, 2 GB RAM
- 0.5 vCPU: 1, 2, 3, 4 GB RAM
- 1 vCPU: 2–8 GB RAM (in 1 GB increments)
- 2 vCPU: 4–16 GB RAM
- 4 vCPU: 8–30 GB RAM
Cost Example: 10 Microservice Pods
Scenario: 10 pods, each requesting 0.5 vCPU / 1 GB RAM, running 24/7 for 30 days:
- vCPU cost: 10 pods × 0.5 vCPU × $0.04048/vCPU-hour × 720 hours = $145.73
- Memory cost: 10 pods × 1 GB × $0.004445/GB-hour × 720 hours = $32.00
- Total: ~$177.73/month
Equivalent EC2 comparison: A single t3.xlarge (4 vCPU, 16 GB) can bin-pack all 10 pods with room to spare. On-Demand cost: ~$134/month. So EC2 would be cheaper at full steady utilization — but Fargate saves money when pods run for only part of the day (batch jobs, preview environments, scheduled workloads).
Cost Optimization Tips
- Right-size resource requests: Fargate charges based on requests, not actual utilization. Over-provisioned requests directly inflate your bill. Use VPA (Vertical Pod Autoscaler) in recommendation mode to identify the right request values.
- Use Fargate Spot: Fargate Spot offers up to 70% discount for interruption-tolerant workloads. Set
capacityType: FARGATE_SPOTin your eksctl config or via theeks.amazonaws.com/fargate-capacity-typeannotation on pods. - Scale to zero for non-production: Use KEDA or scheduled HPA scaling to run zero pods during off-hours in staging/dev namespaces — you only pay while pods are running.
- Minimize sidecar containers: Each sidecar adds to the pod's billed resource request. Use native Fargate logging (aws-observability ConfigMap) instead of Fluent Bit sidecars where possible.
- Avoid over-specifying memory limits: If your actual memory use is 300 MB but you request 1 GB, you're paying for 1 GB. Profile your apps and set requests close to p99 usage.
Frequently Asked Questions
Can I mix Fargate and EC2 nodes in the same EKS cluster?
Yes, this is the most common production pattern. You can have one or more managed node groups for workloads that need DaemonSets, GPUs, or privileged containers, and Fargate profiles for stateless microservices and batch jobs. Pods are scheduled based on profile matching — if a pod matches a Fargate profile, it goes to Fargate; otherwise it lands on EC2 nodes (assuming node selectors or taints don't conflict).
Why is my pod stuck in Pending on a Fargate cluster?
Common causes: (1) No Fargate profile matches the pod's namespace or labels — check kubectl describe pod <name> for a "no Fargate profile found" event. (2) The Fargate profile's subnets don't have enough available IP addresses. (3) The pod requests a resource combination not supported by Fargate (e.g., requesting more than 30 GB RAM or a GPU). (4) The pod has a nodeSelector or affinity that conflicts with the Fargate virtual node.
How do I run a CronJob on Fargate?
Kubernetes CronJobs create Job pods, which are regular pods. As long as the job's namespace matches a Fargate profile, the job pods will run on Fargate. Fargate is particularly cost-effective for CronJobs because you pay only for the seconds the job pod is actually running — no idle EC2 node capacity needed.
Does Horizontal Pod Autoscaler work on Fargate?
Yes, HPA works exactly the same on Fargate. The HPA controller scales the number of pod replicas based on CPU/memory metrics or custom metrics. Each new replica pod is scheduled on Fargate if it matches a profile. Note that Fargate pod startup takes 30–60 seconds (microVM provisioning), so HPA scale-out is slower than on a pre-warmed EC2 node. Plan for this latency in your scaling policies — use a lower target utilization threshold to trigger scaling earlier.
Can I run stateful applications (databases) on Fargate?
Technically yes, via EFS, but it's generally not recommended for write-heavy databases because EFS latency (~1–3ms) is much higher than EBS gp3 (~0.1–0.2ms). For read-heavy workloads or applications that tolerate slightly higher storage latency (Elasticsearch warm tier, configuration stores, shared asset storage), EFS on Fargate works well. For PostgreSQL, MySQL, or Redis, use RDS/ElastiCache or run the database on EC2 nodes with EBS volumes.