⚡ ~/naveed Tech Blog
⚡ Portfolio Home ✍️ Engineering Blog Deep Dives 🎯 Interview Hub 1,000+ Scenarios ☸️ Kubernetes Mastery Hub 24 Modules 🎮 DevOps Arcade & Quizzes Subnet Blitz ⚡ 🗺️ DevOps Roadmaps PDFs & Guides 🤖 Morpheus Analysis AI Quant ↗ 🛠️ Developer Tools Utilities 🧪 Labs & Experiments 📄 Interactive CV & Certs 🔗 All Links & Socials ⚡ Join The Dispatch (Weekly SRE Newsletter) →

How to Deploy Kubernetes on AWS: Production EKS Architecture, Terraform & Best Practices

How to Deploy Kubernetes on AWS: Production EKS Architecture, Terraform & Best Practices

Deploying Kubernetes on AWS in enterprise production requires far more than spinning up a default cluster with public subnets. From resilient multi-AZ VPC design and Karpenter v1 autoscaling to EKS Pod Identity and zero-downtime upgrades, here is the battle-tested architectural blueprint for deploying enterprise-grade Kubernetes on AWS.

1. Decision Matrix: Amazon EKS vs. Self-Managed Kubernetes (kOps / kubeadm)

When architects evaluate how to deploy Kubernetes on AWS, the initial question is often: Should we use managed Amazon EKS or deploy self-hosted control planes with kOps or kubeadm on raw EC2?

In 2026, the operational reality is unambiguous: For 99% of enterprise production workloads, Amazon EKS is the undisputed industry standard. Running self-managed control plane instances introduces heavy operational toil without business value:

Architectural Capability Amazon EKS (Managed) Self-Managed (kOps / kubeadm on EC2)
Control Plane SLA 99.95% AWS Financed SLA 0% (Team bears 100% on-call burden)
etcd Quorum & Maintenance Fully automated snapshots & disk tuning Manual EBS volume tuning, defrag, & backup scripts
Cluster Upgrades One-click blue/green API server rollout Manual rolling master node OS & kube-apiserver upgrades
AWS IAM Native Auth EKS Pod Identity & Access Entries Requires custom webhook token authenticators
Base Control Plane Cost $0.10/hour ($73/month flat fee) 3x EC2 instances (t4g.xlarge ~ $96/month) + EBS storage

As demonstrated by the cost breakdown, self-hosting etcd and API servers on three multi-AZ EC2 instances actually costs more in pure compute and storage fees ($96+/mo) than the $73/mo managed EKS cluster fee—before counting the hundreds of engineering hours spent troubleshooting split-brain etcd quorums during AZ network blips.

⚡ EXCLUSIVE SRE & KUBERNETES PLAYBOOK

The 970+ Incident Handbook & K8s Runbooks are exclusive to subscribers — unlock for FREE

Get battle-tested production runbooks, AWS architecture blueprints, and zero-downtime CI/CD post-mortems sent directly to your inbox. 100% free, no spam.

2. Production VPC & Subnet Architecture (Avoiding IP Starvation)

A resilient Kubernetes deployment starts at Layer 3 networking. Placing worker nodes or pods in public subnets is a critical security vulnerability. An enterprise AWS VPC architecture requires a dedicated 3-tier layout distributed across at least 3 Availability Zones:

The AWS VPC CNI IP Exhaustion Trap

Unlike overlay networks (such as Calico or Flannel) which allocate virtual pod IPs from private CIDRs like 10.244.0.0/16, the default AWS VPC CNI assigns routable private IP addresses directly from your VPC subnets. If you deploy an EKS cluster in a standard /24 subnet (251 usable IPs), a sudden scaling event or Karpenter surge can exhaust all available IPs in minutes, blocking node initialization and pod scheduling.

To eliminate IP starvation, you must apply two configurations:

  1. Enable VPC CNI Prefix Delegation: Set ENABLE_PREFIX_DELEGATION=true on the aws-node daemonset. Instead of allocating single IP addresses to secondary Elastic Network Interfaces (ENIs), AWS allocates contiguous /28 IPv4 prefixes (16 IPs per prefix slot), dramatically multiplying pod density per EC2 node.
  2. Mandatory Kubernetes Subnet Tags: AWS Load Balancer Controller requires discovery tags to automatically place internal and external ALBs/NLBs into the correct subnets:
    # Public Subnets (for internet-facing ALBs):
    kubernetes.io/role/elb = "1"
    
    # Private App Subnets (for internal ALBs & Karpenter discovery):
    kubernetes.io/role/internal-elb = "1"
    karpenter.sh/discovery = "production-eks-cluster"

For a complete deep-dive into CIDR allocation and calculating AWS-reserved IP addresses, review our guide on AWS VPC Subnetting & CIDR Architecture.

3. Control Plane Security & EKS Pod Identity

Securing the control plane and establishing fine-grained access control is the difference between an enterprise environment and a vulnerable sandbox.

A. Private API Server Endpoint Access

By default, AWS EKS provisions a public Kubernetes API endpoint. In production, configure the cluster with Endpoint Public Access: Enabled (restricted to trusted corporate CIDRs or VPNs) and Endpoint Private Access: Enabled. This ensures that node-to-master communication remains entirely contained inside AWS private network routes.

B. Secret Envelope Encryption with AWS KMS

Kubernetes Secrets stored in etcd are merely base64-encoded by default. AWS EKS provides native envelope encryption using AWS Key Management Service (KMS). Every Secret written to etcd is encrypted with an automated Customer Managed Key (CMK) before storage.

C. Modern IAM: EKS Pod Identity vs. Legacy IRSA

Historically, granting Kubernetes pods AWS IAM permissions required IAM Roles for Service Accounts (IRSA), which involved registering an IAM OIDC identity provider, managing intricate OIDC thumbprints, and crafting verbose trust policy conditions with oidc.eks.region.amazonaws.com/id/...:sub.

EKS Pod Identity replaces this overhead. Powered by an AWS-managed in-cluster daemon, EKS Pod Identity allows pods to assume IAM roles without OIDC federation:

# Assume role trust policy for EKS Pod Identity:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "pods.eks.amazonaws.com"
      },
      "Action": [
        "sts:AssumeRole",
        "sts:TagSession"
      ]
    }
  ]
}

Hands-On Lab Companion

Want to run this live in your own AWS account? Check out our step-by-step hands-on runbook on DevOps Lab with copy-pasteable Terraform manifests, Helm configs, and validation commands:

Open Hands-On Lab: Deploy EKS with Terraform →

4. Modern Compute Architecture: Karpenter v1 vs. Managed Node Groups

Traditional Kubernetes deployments on AWS rely on Managed Node Groups (MNG) backed by AWS Auto Scaling Groups (ASGs) and the legacy Kubernetes Cluster Autoscaler. This model exhibits severe operational bottlenecks:

Karpenter v1 completely redefines Kubernetes compute on AWS. Rather than interacting with ASGs, Karpenter speaks directly to the Amazon EC2 Fleet API:

How Karpenter Optimizes AWS Spend

When unscheduled pods enter the queue, Karpenter inspects their CPU/RAM requests, topology spread constraints, and node affinities. It calculates the mathematically optimal EC2 instance type (mixing On-Demand, Spot, AMD, and ARM Graviton) and provisions the machine in under 45 seconds. During low-traffic periods, Karpenter automatically evicts pods and consolidates workloads onto smaller, cheaper instances.

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: general-workloads
spec:
  template:
    spec:
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      requirements:
        - key: "karpenter.k8s.aws/instance-category"
          operator: In
          values: ["c", "m", "r"]
        - key: "karpenter.k8s.aws/instance-generation"
          operator: Gt
          values: ["5"]
        - key: "karpenter.sh/capacity-type"
          operator: In
          values: ["spot", "on-demand"]
        - key: "kubernetes.io/arch"
          operator: In
          values: ["amd64", "arm64"]
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1m

5. End-to-End Terraform / OpenTofu Production Blueprint

Declarative Infrastructure as Code (IaC) guarantees reproducible, auditable deployments. Below is the production Terraform configuration using the battle-tested terraform-aws-modules/eks/aws module:

# main.tf - Production AWS EKS Deployment
terraform {
  required_version = ">= 1.6.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.50"
    }
  }
}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.8"

  name = "production-k8s-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.0.32.0/19", "10.0.64.0/19", "10.0.96.0/19"]
  public_subnets  = ["10.0.0.0/22", "10.0.4.0/22", "10.0.8.0/22"]

  enable_nat_gateway     = true
  single_nat_gateway     = false # Multi-AZ NAT for high availability
  enable_dns_hostnames   = true
  enable_dns_support     = true

  public_subnet_tags = {
    "kubernetes.io/role/elb" = "1"
  }

  private_subnet_tags = {
    "kubernetes.io/role/internal-elb" = "1"
    "karpenter.sh/discovery"          = "production-k8s-cluster"
  }
}

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.10"

  cluster_name    = "production-k8s-cluster"
  cluster_version = "1.32"

  cluster_endpoint_public_access       = true
  cluster_endpoint_public_access_cidrs = ["203.0.113.0/24"] # Whitelist office CIDR
  cluster_endpoint_private_access      = true

  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets

  # Core EKS Addons
  cluster_addons = {
    coredns = {
      most_recent = true
    }
    kube-proxy = {
      most_recent = true
    }
    vpc-cni = {
      most_recent    = true
      before_compute = true
      configuration_values = jsonencode({
        env = {
          ENABLE_PREFIX_DELEGATION = "true"
          WARM_PREFIX_TARGET       = "1"
        }
      })
    }
    eks-pod-identity-agent = {
      most_recent = true
    }
  }

  # Minimal system node group to host Karpenter and CoreDNS
  eks_managed_node_groups = {
    system = {
      name           = "system-nodes"
      instance_types = ["m6i.large", "m7i.large"]
      min_size       = 2
      max_size       = 4
      desired_size   = 2

      labels = {
        role = "system"
      }

      taints = {
        CriticalAddonsOnly = {
          key    = "CriticalAddonsOnly"
          value  = "true"
          effect = "NO_SCHEDULE"
        }
      }
    }
  }
}

6. Day-2 Ingress & Declarative GitOps Integration

Once the control plane and nodes are healthy, production workloads require declarative traffic management and automated deployments:

A. AWS Load Balancer Controller (TargetGroupBinding)

Do not use the deprecated in-tree Kubernetes cloud provider service load balancers. Instead, install the AWS Load Balancer Controller. It reconciles standard Kubernetes Ingress resources into AWS Application Load Balancers (ALB) and Service type: LoadBalancer into Network Load Balancers (NLB).

Crucially, use IP Target Mode (alb.ingress.kubernetes.io/target-type: ip). In IP mode, the ALB routes HTTP traffic directly to pod IP addresses over the VPC network, eliminating the extra network hop, SNAT overhead, and port collisions caused by legacy NodePort routing.

B. GitOps with ArgoCD

Never run kubectl apply directly from local developer machines. Deploy ArgoCD inside your cluster to continuously synchronize manifests from Git. For complete runbooks on setting up zero-drift GitOps pipelines, review our guide: GitOps with ArgoCD: Eliminating Configuration Drift.

7. Top 5 Production Pitfalls & How to Prevent Them

During real-world infrastructure audits and SRE on-call rotations, these five recurring configuration errors account for over 80% of cluster downtime:

1. Open 0.0.0.0/0 Public API Endpoint

Exposing the Kubernetes API server endpoint to the open internet exposes your cluster to credential stuffing, Zero-Day CVE probes, and DDoS attacks. Always restrict public access CIDRs or disable public access entirely and use an internal VPN or AWS Systems Manager (SSM) bastion.

2. Omitting PodDisruptionBudgets (PDBs)

When Karpenter consolidates nodes or AWS executes a rolling node AMI update, it drains existing nodes. If your applications lack a PodDisruptionBudget with minAvailable: 1 or maxUnavailable: 25%, node drain operations can evict all pod replicas simultaneously, triggering instantaneous user-facing 502/504 outages.

3. Hardcoded Long-Lived AWS Access Keys

Storing AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY inside Kubernetes Secrets or ConfigMaps violates cloud security baselines. Always bind IAM roles dynamically using EKS Pod Identity or AWS Secrets Manager with external secrets operators.

4. Ignoring topologySpreadConstraints Across AZs

By default, the Kubernetes scheduler does not guarantee that your pod replicas are evenly split across AWS Availability Zones. If an entire AZ experiences a power outage or hardware degradation and all your replicas were co-located on nodes in that AZ, your service goes down. Always enforce topologySpreadConstraints matching topology.kubernetes.io/zone with maxSkew: 1.

5. Unbounded Single NAT Gateway Bottlenecks

Routing egress traffic from all 3 private AZs through a single NAT Gateway creates both a single point of failure and cross-AZ data transfer fees ($0.01 per GB in each direction). For mission-critical clusters, allocate one NAT Gateway per public AZ.

For real-world on-call scenarios covering these exact failure modes in high-pressure interviews, practice with our Zero-Downtime Amazon EKS Minor Version Upgrade Scenario on the AWS Interview Handbook.

8. Frequently Asked Questions (FAQ)

What is the best way to deploy Kubernetes on AWS in production?
The industry standard is Amazon Elastic Kubernetes Service (EKS) provisioned with Terraform or OpenTofu. Pairing EKS with Karpenter v1 for just-in-time autoscaling, EKS Pod Identity for least-privilege IAM, and AWS VPC CNI with prefix delegation provides the optimal balance of 99.95% control plane availability, high security, and minimal operational toil.
Should I use Amazon EKS or self-managed Kubernetes (kubeadm / kOps) on EC2?
Amazon EKS is overwhelmingly recommended for production. Self-managing control plane nodes with kubeadm or kOps requires managing multi-AZ EC2 instances, etcd disk tuning, automated backup scripts, and manual control plane upgrades. The $0.10/hour ($73/month) EKS fee is lower than the EC2 compute cost of three dedicated master instances, and it completely eliminates control plane on-call paging.
How do you avoid VPC IP exhaustion when deploying Kubernetes on AWS?
AWS VPC CNI assigns native VPC IPs directly to every pod. To prevent small subnets from running out of IPs during scaling surges: (1) Enable VPC CNI Prefix Delegation (ENABLE_PREFIX_DELEGATION=true) to assign /28 IP blocks (16 IPs) per ENI slot, and (2) Attach secondary non-routable CIDR blocks (e.g. 100.64.0.0/10 CGNAT) reserved strictly for pod interfaces.
What is the difference between EKS Pod Identity and IRSA?
IAM Roles for Service Accounts (IRSA) requires OIDC identity provider registration, managing thumbprints, and editing lengthy IAM trust policies with OIDC ARN matching. EKS Pod Identity uses an in-cluster agent and direct AWS APIs, eliminating OIDC plumbing, reducing IAM policy overhead, and simplifying cross-cluster IAM role sharing.
Why use Karpenter instead of Kubernetes Cluster Autoscaler?
Cluster Autoscaler is tightly coupled to AWS Auto Scaling Groups (ASGs), taking 3 to 6 minutes to launch nodes while restricted to rigid instance configurations. Karpenter communicates directly with the Amazon EC2 fleet API, selects the cheapest optimal instance type within seconds, and continuously consolidates underutilized nodes to reduce cloud compute bills by up to 40%.

Key Architecture Takeaways

Naveed Ahmed

Naveed Ahmed (Kumbhar)

Senior DevOps & Cloud Engineer with 10+ years specializing in AWS, Kubernetes, Platform Engineering, SRE incident response, and autonomous AI infrastructure agents.

Have a technical challenge or architecture question? Connect on LinkedIn →