AWS Networking & Kubernetes at Scale
VPC foundations, EKS internals, service mesh, ingress, autoscaling
VPC network foundations
After this you can design a production VPC and reason about connectivity options.
The VPC is the floor every Cursor service stands on. Get the address plan and the subnet topology wrong on day one and you pay for it in every migration, every peering and every region you add later.
A Software Engineer, Infrastructure at Cursor owns these network foundations for a product serving over 1M daily active users. The design-round bar is not reciting AWS terms. It is showing you can lay out a VPC that survives growth across regions and accounts without an IP renumbering project and that you know exactly where traffic is allowed to flow and where it is blocked.
Subnets, routing and AZ layoutthe skeleton
A VPC is one big CIDR block carved into subnets, one per Availability Zone per tier. A subnet is public if its route table sends 0.0.0.0/0 to an Internet Gateway and private if egress instead goes to a NAT Gateway. Production load balancers and bastions sit in public subnets. Everything that actually runs your workloads, EKS nodes included, sits in private subnets.
- Subnet
- A CIDR slice bound to exactly one AZ. Spread each tier across at least 3 AZs so one zone failing costs you a third of capacity, not the service.
- Route table
- What makes a subnet public or private. The default route's target (IGW vs. NAT vs. nothing) is the whole distinction.
- Internet Gateway
- One per VPC. Gives public subnets two-way internet reachability for inbound traffic to load balancers.
- NAT Gateway
- Lets private subnets reach out (pull images, hit APIs) without being reachable from outside. Per-AZ for HA and a real line item on the bill.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Top to bottom: where traffic enters, where it's filtered and what carries it.
A single NAT Gateway is a cost trap and a cross-AZ data charge in disguise. One NAT per AZ avoids the data-transfer toll for traffic that would otherwise hairpin across zones and it removes a single point of failure. At 1M+ DAU scale, NAT egress is a number you will be asked to defend, so know it cold.
Security groups vs. NACLsstateful vs. stateless
Both filter traffic, but they live at different layers and behave differently. Security groups attach to an interface and are stateful: allow a connection out and the return packets come back automatically. NACLs sit at the subnet boundary and are stateless: you must write both directions, including ephemeral return ports or the reply is silently dropped.
- Property
- Attaches to
- Security group
- ENI / instance / pod
- NACL
- Subnet
- Property
- State
- Security group
- Stateful (return traffic auto-allowed)
- NACL
- Stateless (must allow both directions)
- Property
- Rules
- Security group
- Allow only
- NACL
- Allow and explicit deny
- Property
- Evaluation
- Security group
- All rules, most-permissive wins
- NACL
- Numbered, first match wins
- Property
- Default reach for
- Security group
- Day-to-day app access control
- NACL
- Coarse subnet guardrail / blocking a bad CIDR
| Property | Security group | NACL |
|---|---|---|
| Attaches to | ENI / instance / pod | Subnet |
| State | Stateful (return traffic auto-allowed) | Stateless (must allow both directions) |
| Rules | Allow only | Allow and explicit deny |
| Evaluation | All rules, most-permissive wins | Numbered, first match wins |
| Default reach for | Day-to-day app access control | Coarse subnet guardrail / blocking a bad CIDR |
Reach for security groups by default; use NACLs as a blunt subnet-wide backstop.
Security groups can reference other security groups as their source, not just IP ranges. "Allow the EKS node SG to reach the database SG on 5432" stays correct as instances churn, where a hand-maintained IP allowlist rots the moment autoscaling replaces a node.
Connectivity at scalepeering, Transit Gateway, PrivateLink
Two VPCs that need to talk can peer directly and that is fine for a handful. The trouble is that peering is non-transitive and the mesh grows as the square of the VPC count. Once you have many VPCs and accounts, a Transit Gateway becomes the hub everything routes through.
Direct 1:1 link between two VPCs.
Non-transitive: A↔B and B↔C does not give A↔C.
Cheap and simple for a small fixed set.
A hub-and-spoke router for many VPCs and accounts.
Transitive routing with route tables per attachment.
Scales the topology; adds per-attachment and per-GB cost.
Exposes one service over a private endpoint.
Consumer reaches it without route/CIDR overlap concerns.
Best when you want a service, not full network reachability.
For reaching AWS services themselves (S3, ECR, Secrets Manager) from private subnets, use VPC endpoints. A gateway endpoint for S3 and DynamoDB or an interface endpoint for the rest keeps that traffic off the NAT path and off the public internet entirely. It cuts NAT cost and shrinks the attack surface in one move.
IP planning so growth doesn't corner youthe decision you can't easily undo
Pods on the EKS VPC CNI each consume a real VPC IP, so a busy cluster burns through address space fast. Plan CIDRs with that appetite in mind and reserve non-overlapping ranges per region and per account up front. Overlapping CIDRs are the thing that quietly blocks peering and Transit Gateway later and the fix is a renumbering project nobody wants.
When asked to design a VPC, draw the address plan before the boxes: "I'd give each region a /16 from a non-overlapping parent range, split into per-AZ public and private /20s and reserve a second /16 per region for CNI growth." Leading with CIDR strategy signals you have operated at scale, where renumbering is the scar everyone has.
Takeaway. Public vs. private is just the default route's target; spread tiers across 3 AZs, default to security groups and reserve NACLs as a blunt backstop and plan non-overlapping CIDRs with CNI's IP appetite in mind before you draw a single box.
Self-check
QYou have two private subnets in different AZs. One can reach the internet to pull container images; the other times out on every outbound call. Both have the same security groups. What's the most likely cause?
IAM and least privilege
After this you can explain blast-radius-contained access for workloads and humans.
IAM is where a small infra team either contains a compromise to one pod or hands an attacker the whole account. On a flat, talent-dense team, you own that boundary directly.
The design-round version of this question is rarely "what is an IAM role." It is "a credential leaks or a pod is compromised - how far does the damage spread and how did your design limit it?" Answer in terms of blast radiusHow much breaks if a change goes wrong; the scope of potential damage. and least privilege and ground it in how workloads actually get credentials on EKS.
Roles, policies and the two policy shapesthe vocabulary, used correctly
- Role
- An assumable identity with no long-lived credentials. Humans, services and pods assume a role to get short-lived, scoped tokens.
- Policy
- The JSON document of allow/deny statements attached to a role or resource. The role is who; the policy is what they can do.
- Identity-based policy
- Attached to a principal (role/user): "this identity may call S3 GetObject on bucket X."
- Resource-based policy
- Attached to the resource (bucket, queue, KMS key): "this account/role may act on me." Enables cross-account access.
Least privilege means each principal carries the narrowest set of actions on the narrowest set of resources that lets it do its one job. Start from deny, add what breaks and scope with conditions (a specific resource ARN, a tag, a source VPC) rather than Resource: "*".
IRSA: pods get their own scoped rolethe EKS-specific answer
The naive way to give a pod AWS access is to attach a policy to the node's instance role. Then every pod on that node inherits it and one compromised container can act as all of them. IRSA (IAM Roles for Service Accounts) fixes this: the pod's Kubernetes service account is bound to an IAM role through an OIDCOpenID Connect. A modern standard that powers single sign-on, built on OAuth. provider, so each workload assumes its own scoped role and gets short-lived credentials.
- Approach
- Node instance role
- Who gets the permissions
- Every pod on the node
- Blast radius
- Whole node's workload set
- Approach
- IRSA / Pod Identity
- Who gets the permissions
- Only the bound service account
- Blast radius
- One workload
- Approach
- Long-lived keys in image
- Who gets the permissions
- Anyone who reads the image
- Blast radius
- Anywhere, until the key is rotated
| Approach | Who gets the permissions | Blast radius |
|---|---|---|
| Node instance role | Every pod on the node | Whole node's workload set |
| IRSA / Pod Identity | Only the bound service account | One workload |
| Long-lived keys in image | Anyone who reads the image | Anywhere, until the key is rotated |
IRSA (or EKS Pod Identity) is the default; node-role permissions and baked-in keys are anti-patterns.
Never bake long-lived access keys into a container image or a committed config. Images get shared, cached and scanned; a key in layer history is a key forever until rotated. Use IRSA for AWS access and a secrets manager for everything else, with rotation on a schedule.
Secrets and rotationno standing credentials in plaintext
- Store secrets in AWS Secrets Manager or SSM Parameter Store, encrypted with KMS and pull them at runtime - not at build time.
- Mount into pods via the Secrets Store CSI driver or External Secrets, scoped per workload via IRSA, so a pod only ever sees its own secrets.
- Rotate on a schedule and after any suspected exposure; prefer credentials that are short-lived by construction over ones you remember to rotate.
Containing blast radius by designboundaries, not just rules
The strongest containment is structural. Separate AWS accounts per environment mean a blown prod credential cannot touch staging and a misconfigured staging policy cannot reach prod data. Organization SCPs (Service Control Policies) sit above every role as a guardrail ceiling: even an over-broad role can't exceed what the SCP permits, so a mistake in one team's policy can't escalate past the account's hard limits.
Frame every IAM answer around the failure: "Assume this credential leaks. With IRSA the attacker gets one workload's narrow role; with separate accounts they can't cross into prod; with an SCP they can't disable logging or leave the region." Reasoning in blast radiusHow much breaks if a change goes wrong; the scope of potential damage. reads as senior; listing policy syntax reads as junior.
Takeaway. Give every workload its own scoped role via IRSA, keep zero long-lived keys in images and contain damage structurally with per-environment accounts under SCP guardrails - then explain your design by walking a leaked credential through it.
Self-check
EKS / Kubernetes internals
After this you can reason about how the cluster actually schedules and runs workloads.
You can't operate a cluster at 1M+ DAU scale by treating it as a black box that "runs containers." When pods go Pending or get OOM-killed at 3am, the person who knows the path from manifest to running process is the one who fixes it.
Control plane vs. data planewho decides vs. who runs
EKS splits cleanly. The control plane (API server, etcd, scheduler, controllers) is AWS-managed and decides what should run where. The data plane (your worker nodes running kubelet and a container runtime) is what you own and pay for and it does the actual running.
- 1You apply a manifest.
kubectl applywrites the desired Deployment to the API server, which persists it in etcd. - 2Controllers reconcile. The Deployment controller creates a ReplicaSet, which creates the Pod objects - still unscheduled.
- 3The scheduler binds. It filters nodes by requests, taints and affinity, scores the survivors and binds each pod to a node.
- 4Kubelet runs it. The node's kubelet sees the bound pod, pulls the image, asks the container runtime to start it and reports status back.
- 5Probes gate traffic. Readiness must pass before the pod joins a Service's endpoints and starts taking requests.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
The path a pod travels - and the readiness gate that decides when it takes traffic.
Requests vs. limitsthe single most consequential knob
Requests are what the scheduler reserves; limits are the hard ceiling the kernel enforces. The gap between them decides scheduling density, eviction order and whether one noisy pod can starve its neighbors.
- Setting
- CPU request
- What it does
- Reserves schedulable CPU; affects bin-packing
- Get it wrong and…
- Too high wastes nodes; too low oversubscribes and throttles
- Setting
- CPU limit
- What it does
- Throttles the container above the ceiling
- Get it wrong and…
- Too low causes latency spikes under load (CPU throttling)
- Setting
- Memory request
- What it does
- Reserves memory for scheduling
- Get it wrong and…
- Too low lets the node oversubscribe memory
- Setting
- Memory limit
- What it does
- Hard cap; exceeding it = OOMKill
- Get it wrong and…
- Too low gets the pod killed under spikes (no throttling for memory)
| Setting | What it does | Get it wrong and… |
|---|---|---|
| CPU request | Reserves schedulable CPU; affects bin-packing | Too high wastes nodes; too low oversubscribes and throttles |
| CPU limit | Throttles the container above the ceiling | Too low causes latency spikes under load (CPU throttling) |
| Memory request | Reserves memory for scheduling | Too low lets the node oversubscribe memory |
| Memory limit | Hard cap; exceeding it = OOMKill | Too low gets the pod killed under spikes (no throttling for memory) |
Memory has no soft throttle - over the limit, the pod dies. CPU over the limit just slows down.
Set request = limit and the pod is Guaranteed and last to be evicted. Set request < limit and it's Burstable. Set neither and it's BestEffort and first to die under node pressure. For latency-critical Cursor services, Guaranteed for the hot path and Burstable for the rest is a defensible split.
CNI and the IP-exhaustion trapwhere EKS networking bites
The AWS VPC CNI gives every pod a real VPC IP from the subnet's range, which makes pod networking native and fast. The cost is that a dense node with many pods consumes many subnet IPs and instances cap how many IPs they can attach by type. Run out of subnet IPs and new pods sit Pending with no obvious error in the app logs - the failure is in the network layer, not the workload.
- Size private subnets for peak pods-per-node times node count, then add headroom for rollouts that double pod count briefly.
- Watch for Pending pods with FailedCreatePodSandbox or IP-allocation events - that's CNI exhaustion, not a scheduling bug.
- Consider prefix delegation on the VPC CNI to pack more pod IPs per node or carve a secondary CIDR for pods specifically.
Health and safe rolloutsprobes and disruption budgets
"Is this process wedged?"
Fail → kubelet restarts the container.
Set too aggressive and healthy-but-slow pods get killed in a loop.
"Can it take traffic right now?"
Fail → pulled from Service endpoints, not restarted.
Gates rollouts so traffic only hits warmed pods.
"Has slow init finished?"
Holds liveness off until the app is up.
Stops liveness from killing apps with long cold starts.
A PodDisruptionBudget caps how many pods of a workload can be voluntarily down at once, so a node drain or cluster upgrade can't take a service below its minimum. Pair PDBs with a RollingUpdate strategy and correct readiness probes and you get zero-downtime deploys; skip them and a routine node rotation becomes an outage.
Stateful workloads are the exception on this platform, run via StatefulSets with stable network identities and per-pod PersistentVolumeClaims (EBS) that follow the pod's ordinal. They don't reschedule across AZs freely, because an EBS volume is AZ-bound, so storage topology becomes part of your HA design.
When a pod is described as "stuck," diagnose out loud by state: Pending means the scheduler can't place it (resources, taints or IP exhaustion); CrashLoopBackOff means it starts then dies (app error or a too-aggressive liveness probe); OOMKilled means it blew the memory limit. Naming the state and its likely causes shows you've actually operated clusters.
Takeaway. The control plane decides and the kubelet runs; requests vs. limits drive scheduling, QoS and OOM behavior; and on EKS the quiet killer is VPC CNI IP exhaustion showing up as Pending pods, not an app error.
Self-check
QA deployment's pods stay Pending during a scale-up even though node CPU and memory dashboards show plenty of headroom. The events mention sandbox creation and IP allocation. What's happening?
Service mesh and ingress
After this you can decide when a mesh earns its keep and how traffic enters the cluster.
A service mesh is one of those tools that solves real problems and creates new ones. The senior signal isn't "I deployed Istio" - it's knowing the exact problems that justify the latency and operational tax and being willing to say no.
What a mesh actually buys youthe case for it
- mTLS everywhere
- Automatic mutual TLS between services, so in-cluster traffic is encrypted and identity-verified without per-app crypto code.
- Resilience policies
- Retries, timeouts and circuit breaking applied uniformly, configured outside the app rather than reimplemented per service.
- Traffic splitting
- Percentage-based routing for canaries and blue-green, decoupled from deploy replica counts.
- Uniform observability
- Consistent golden-signal metrics and traces for every service, even ones whose authors instrumented nothing.
Istio vs. Linkerd vs. Ciliumand the sidecar tax
The classic mesh injects a sidecar proxy next to every pod, which intercepts all traffic. That's where the features come from and also where the cost comes from: a proxy per pod means extra CPU, memory and a couple of hops of latency on every request. The newer move is sidecarless - push mTLS and policy into the kernel with eBPF, so there's no per-pod proxy on the hot path.
- Mesh
- Istio
- Model
- Sidecar (Envoy) or ambient/eBPF mode
- Trade
- Most features and traffic control; heaviest to run and tune
- Mesh
- Linkerd
- Model
- Sidecar (lightweight Rust micro-proxy)
- Trade
- Simple, low overhead, opinionated; fewer advanced knobs
- Mesh
- Cilium
- Model
- eBPF, largely sidecarless
- Trade
- Lowest per-request overhead via kernel; ties mesh to CNI/eBPF
| Mesh | Model | Trade |
|---|---|---|
| Istio | Sidecar (Envoy) or ambient/eBPF mode | Most features and traffic control; heaviest to run and tune |
| Linkerd | Sidecar (lightweight Rust micro-proxy) | Simple, low overhead, opinionated; fewer advanced knobs |
| Cilium | eBPF, largely sidecarless | Lowest per-request overhead via kernel; ties mesh to CNI/eBPF |
Sidecar = features and per-pod cost. eBPF/sidecarless = less latency and overhead, more kernel coupling.
Every sidecar hop adds tail latency and Cursor is latency-obsessed because completions and agent steps are interactive. A few milliseconds per in-cluster hop, multiplied across a request fan-out and concentrated in p99, is exactly the cost the eBPF/sidecarless approach exists to remove. Quantify it before you adopt a sidecar mesh.
How traffic enters the clusteringress and load balancers
External traffic reaches pods through ingress. An ingress controller watches Ingress (or Gateway API) resources and programs an AWS load balancer to match. On EKS the AWS Load Balancer Controller provisions an ALB for L7 routing or an NLB for L4 and the L4-vs-L7 choice shapes what you can do at the edge.
Routes on IP/port; passes bytes through.
Lowest latency, handles raw TCP/UDP and TLS passthrough.
Reach for it when you need throughput or non-HTTP protocols.
Understands HTTP: path/host routing, headers, TLS termination.
Enables host/path rules, WAF attach and request-aware routing.
Reach for it when routing decisions depend on the request.
The honest counterpointwhen NOT to run a mesh
A mesh adds a control plane to operate, upgrade and debug; it adds latency; and it makes failures harder to reason about because the proxy is now in the path of every request. For a small number of services, you can get mTLS from the edge plus app-level TLS, retries from a shared client library and traffic splitting from the ingress alone - without the mesh tax.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Same capabilities, two ways to get them - match the tool to the actual problem.
If asked "would you add a service mesh?", resist the reflexive yes. Say: "What problem are we solving - mTLS, uniform retries or canary routing? For a few services I'd get those from ingress plus a client library and skip the per-pod proxy. I'd reach for a mesh once we have enough services that policy drift and inconsistent observability become the real cost and I'd lean eBPF/sidecarless to protect p99." Naming the threshold and the latency trade is the bar.
Takeaway. A mesh buys mTLS, uniform retries and canary routing - at the price of a per-pod proxy on every hop; justify it by the problem and the p99 latency math, lean eBPF/sidecarless when you do and pick L7 (ALB) when routing depends on the request, L4 (NLB) when it doesn't.
Self-check
Autoscaling that holds at scale
After this you can scale on the right signals without over-provisioning or thrashing.
Autoscaling for 1M+ DAUs is a control-systems problem. Scale on the wrong signal and you over-provision a fortune; tune it badly and you get oscillation that hurts more than the load ever would.
Three layers, three jobspods, sizing and nodes
Scaling happens at distinct layers and conflating them is a common interview stumble. One scales pod count, one scales each pod's resources and one scales the nodes underneath.
- Tool
- HPA
- Scales
- Pod replica count
- Trigger
- Metric (CPU, custom, external)
- Note
- The everyday horizontal knob
- Tool
- VPA
- Scales
- A pod's requests/limits
- Trigger
- Observed usage over time
- Note
- Right-sizes; conflicts with HPA on the same metric
- Tool
- Cluster Autoscaler
- Scales
- Node count (fixed node groups)
- Trigger
- Pending pods that don't fit
- Note
- Adds nodes from predefined groups
- Tool
- Karpenter
- Scales
- Nodes (just-in-time, any type)
- Trigger
- Pending pods
- Note
- Picks instance types/sizes per workload; faster, cheaper bin-packing
| Tool | Scales | Trigger | Note |
|---|---|---|---|
| HPA | Pod replica count | Metric (CPU, custom, external) | The everyday horizontal knob |
| VPA | A pod's requests/limits | Observed usage over time | Right-sizes; conflicts with HPA on the same metric |
| Cluster Autoscaler | Node count (fixed node groups) | Pending pods that don't fit | Adds nodes from predefined groups |
| Karpenter | Nodes (just-in-time, any type) | Pending pods | Picks instance types/sizes per workload; faster, cheaper bin-packing |
HPA and VPA scale workloads; Cluster Autoscaler and Karpenter scale the nodes those workloads land on.
Don't run HPA and VPA on the same resource metric - they fight. HPA adds pods because CPU is high while VPA shrinks each pod's CPU request and they oscillate against each other. Use VPA for right-sizing memory/baseline and HPA for horizontal scale on a different signal or keep them on separate workloads.
Scale on SLO-relevant signalsnot just CPU
CPU is a proxy and often a bad one. A request queue can be backing up while CPU sits at 40%, because the work is I/O-bound or waiting on a model backend. Scale on the thing your users actually feel: p99 latency, queue depth or in-flight request concurrency.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Ranked by how faithfully each tracks user-felt pain at 1M+ DAU scale.
Scale when p99 crosses the SLO threshold.
Directly tied to user-felt pain.
Watch for noise; smooth before acting.
Best for async and request-buffered work.
Rising backlog is an early, honest signal.
Catches I/O-bound load CPU misses.
In-flight requests per pod as the target.
Stable and intuitive to reason about capacity.
Pairs well with a known per-pod capacity.
Surviving spiky, bursty trafficscale-up latency is the enemy
The hard truth of bursty load is that adding a node isn't instant. A new EC2 instance must boot, join the cluster, pull images and pass readiness, which can take minutes - and a traffic spike doesn't wait. So you buy time with headroom and warm capacity rather than reacting from zero.
- Keep a headroom buffer (over-provisioning / pause pods) so a spike lands on already-running capacity while new nodes boot.
- Use warm pools or fast-launching node types so scale-up latency is seconds, not minutes.
- Pre-pull large images to nodes so image fetch isn't on the critical path during a spike.
- Set HPA min replicas above your true floor for spiky services, so you never scale up from a cold, single-pod state.
Avoiding thrashthe feedback-loop failure
An autoscaler that reacts too fast oscillates: it scales up on a brief spike, the metric drops, it scales down, the load returns and it scales up again - churning pods and nodes while serving worse than a steady fleet would. Stabilization windows and asymmetric up/down behavior tame it.
- Scale-up fast, scale-down slow
- React quickly to protect users; remove capacity conservatively to avoid yanking it back.
- Stabilization window
- Require the signal to hold for a window before acting, so transient blips don't trigger scaling.
- Sane min/max
- A min that absorbs normal variance and a max that caps cost and protects downstream dependencies.
- Protect dependencies
- Cap max so a scale-up storm can't overwhelm a database or model backend that can't scale with you.
Tie the whole answer to cost-vs-reliability, which is the judgment Cursor's infra loop tests. "I'd scale pods on p99 latency, not CPU, run Karpenter for cheap just-in-time nodes, hold a small headroom buffer so spikes don't wait on boot time and use a stabilization window plus a hard max so we don't thrash or stampede the model backend." That sentence shows you optimize spend and reliability together, not one at the other's expense.
Takeaway. Scale pods on SLO signals (p99, queue depth) not raw CPU, scale nodes with Karpenter, buy time for spikes with headroom and warm pools and stop oscillation with stabilization windows plus a hard max that protects downstream dependencies.
Self-check
QYour service is CPU-light but calls a slow model backend. Under load, p99 latency climbs and the request queue grows, yet CPU stays near 40% so the HPA never scales up. How do you fix the autoscaling and what do you watch for?