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.

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.
When to use Fargate: Fargate is ideal for variable, bursty workloads; microservices where isolation matters; teams that want to reduce operational overhead; and batch or event-driven jobs. For workloads requiring GPUs, high network throughput (>25 Gbps), DaemonSets, or custom node-level tooling, managed node groups are the better fit.

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 Fargate
  • namespace: default — catch-all for default workloads
  • namespace: production, labels: {tier: backend} — targeted profile for specific service tier
  • namespace: 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
Mixed cluster strategy: Many production clusters use Fargate for stateless microservices and batch jobs (where isolation and zero node-management matter) and a small managed node group for system workloads like monitoring agents, admission webhooks, and any workload needing DaemonSets.

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 SecurityGroupPolicy CRD, 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-storage resource 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
EFS Access Points: When using 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
ALB Controller must run on Fargate or a node group: If your cluster is pure Fargate, the AWS Load Balancer Controller deployment itself needs to match a Fargate profile. Ensure the 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:

  1. Create a Fargate profile for the kube-system namespace (already covered in the cluster setup above).
  2. Patch the CoreDNS deployment to remove the eks.amazonaws.com/compute-type: ec2 annotation 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-logging ConfigMap — 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
Sidecar resource overhead: Each Fluent Bit sidecar adds CPU and memory to your Fargate pod's billed size. A 0.1 vCPU / 128 MB sidecar will round up the pod's total to the next Fargate size tier. Budget this into your resource planning — for a 0.5 vCPU / 1 GB app, adding the sidecar brings the total to 0.6 vCPU / 1.128 GB, which rounds to the 1 vCPU / 2 GB Fargate tier.

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_SPOT in your eksctl config or via the eks.amazonaws.com/fargate-capacity-type annotation 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.