Showing posts with label Kubernetes. Show all posts
Showing posts with label Kubernetes. Show all posts

Tuesday, 21 July 2026

Kubernetes Custom Resource Definition (CRD)


Why are Custom Resource Definitions (CRDs) stored in etcd?


Custom Resource Definitions (CRDs) are stored in etcd because etcd serves as the single source of truth and persistent data store for the entire Kubernetes control plane. Storing CRDs in etcd allows the kube-apiserver to dynamically register, validate, and manage custom resources exactly like native objects without requiring a separate database.

Why Kubernetes Stores CRDs in etcd

  • Native API Integration: The kube-apiserver inherently reads from and writes to etcd. Storing CRDs here allows custom objects to instantly leverage native features like kubectl support, RBAC security, and namespaces.
  • Consistent State: Like Pods or Deployments, custom resources represent the "desired state" of a system. etcd provides the strong consistency and distributed consensus needed to safely store this state across cluster nodes.
  • Watch Mechanism: Custom controllers and operators rely on the Kubernetes watch API to listen for resource changes. Because etcd natively supports watch events, controllers can instantly react when a custom resource is created, updated, or deleted.

How custom controllers interact with the stored CRD data using the reconciliation loop?

Custom controllers interact with etcd through a continuous synchronization mechanism called the reconciliation loop. This loop constantly drives the actual state of the cluster toward your desired state.

Here is exactly how custom controllers interact with stored CRD data:

1. The Relationship to CRDs

A CRD (Custom Resource Definition) acts as the database schema. It defines the structure and validation rules for your custom data. When you create a CR (Custom Resource) based on that CRD, its data is stored directly in etcd.

The custom controller is the active software component (the brain) that watches that specific CR data and takes action based on what is stored.

2. The Reconciliation Loop Step-by-Step

The controller executes a continuous loop, often formalized as a Reconcile() function, which follows a Observe, Analyze, Act workflow:   

    +--------------------------------------------+
    |                                            |
    |               1. OBSERVE                   |
    |     (Watch etcd via API Server)            |
    |                                            |
    +---------------------+----------------------+
                          |
                          v
    +---------------------+----------------------+
    |                                            |
    |               2. ANALYZE                   |
    |    (Compare Desired vs. Actual State)      |
    |                                            |
    +---------------------+----------------------+
                          |
                          v
    +---------------------+----------------------+
    |                                            |
    |                 3. ACT                     |
    |     (Create/Update/Delete Infrastructure)  |
    |                                            |
    +--------------------------------------------+


  • Step 1: Observe (The Watch Mechanism): Instead of constantly polling etcd, the controller establishes a long-lived HTTP connection (a watch) to the kube-apiserver. When a user creates, updates, or deletes a CR in etcd, the API server instantly broadcasts an event. The controller captures this event and queues the resource's key for processing.
  • Step 2: Analyze (Diffing the State): The controller fetches the full CR data from etcd. It reads the .spec section, which represents the user's desired state (e.g., "replicas": 3). It then looks at the real-world cluster infrastructure to see the actual state (e.g., only 2 pods are running).
  • Step 3: Act (Fixing the Drift): The controller executes the necessary logic to close the gap. In this case, it talks to the API server to spin up a 3rd pod.
  • Step 4: Update Status: Once the action is complete, the controller updates the .status subresource of the CR. The API server writes this new status back to etcd, confirming the actual state now matches the desired state.

Key Characteristics

  • Level-Triggered: The loop reacts not just to isolated events, but to the current state of the system. If the controller crashes and restarts, it reads etcd to immediately understand the current state and resume fixing any drift.
  • Asynchronous: The user gets an immediate "Success" response from the API server when saving a CR to etcd. The controller processes the request in the background asynchronously.



Friday, 17 July 2026

Accessing AWS EKS Cluster from EC2


When an EC2 instance (acting as a CI/CD runner, bastion, or deployment node) runs Terraform or Helm to interact with a private EKS cluster, it needs two things: 
  • network visibility (which it gets by being in the same VPC or connected network) and 
  • proper IAM-to-Kubernetes mappings

AWS modern access management feature uses EKS Access Entries. This native API feature entirely replaces the messy, deprecated aws-auth ConfigMap.

Here is how to configure the IAM Role and wire it into the cluster's auth configuration using Terraform.

1. The EC2 IAM Role (AWS Side)


The EC2 instance does not need any specific EKS admin permissions attached directly to its IAM role policies. It just needs a standard IAM Role that it can assume via an EC2 Instance Profile. The actual Kubernetes cluster access is granted inside EKS by referencing this role's Amazon Resource Name (ARN).  

# 1. Create the IAM Role for the EC2 Instance
resource "aws_iam_role" "cicd_runner" {
  name = "eks-cicd-runner-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action    = "sts:AssumeRole"
        Effect    = "Allow"
        Principal = { Service = "ec2.amazonaws.com" }
      }
    ]
  })
})

# 2. Create the Instance Profile so the EC2 can use the role
resource "aws_iam_instance_profile" "cicd_runner_profile" {
  name = "eks-cicd-runner-profile"
  role = aws_iam_role.cicd_runner.name
}

2. Wiring it into the Cluster (EKS Side)


To authorize this IAM role to run Helm deployments or manage Kubernetes resources via Terraform, you use EKS Access Entries (aws_eks_access_entry) combined with Access Policy Associations (aws_eks_access_policy_association).  

⚠️ Prerequisite: Ensure your aws_eks_cluster resource has authentication_mode = "API_AND_CONFIG_MAP" or "API" enabled so it accepts Access Entries.

The Terraform Configuration

# 1. Define the Access Entry mapping the IAM Role ARN to EKS
resource "aws_eks_access_entry" "cicd_runner_entry" {
  cluster_name  = "my-cluster-name"
  principal_arn = aws_iam_role.cicd_runner.arn
  type          = "STANDARD" # Standard workflow for users/roles
}

# 2. Attach an AWS-managed access policy to the Access Entry
resource "aws_eks_access_policy_association" "cicd_admin_mapping" {
  cluster_name  = "my-cluster-name"
  principal_arn = aws_iam_role.cicd_runner.arn
  policy_arn    = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"

  access_scope {
    type = "cluster"
  }
}


Another example (in the cluster's Terraform source code):

# read-only access entry for the in-VPC runner's instance role, so operators can kubectl the
# private cluster via SSM-on-the-runner for diagnostics without an out-of-band `aws eks create-access-entry` grant each time. AmazonEKSAdminViewPolicy = read all resources incl.
# CRDs, excluding Secrets — safe for diagnostics. The runner role is defined in another repo
# (applications/test/github-runner); it also gets eks:DescribeCluster there so `aws eks update-kubeconfig`
# works. Writes still require github-actions-role (the CD/harness OIDC principal above).

resource "aws_eks_access_entry" "runner_readonly" {
  cluster_name  = local.cluster_name
  principal_arn = "arn:aws:iam::1234567890123:role/github-runner-role"
  type          = "STANDARD"

  depends_on = [module.eks]
}

resource "aws_eks_access_policy_association" "runner_readonly" {
  cluster_name  = local.cluster_name
  policy_arn    = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSAdminViewPolicy"
  principal_arn = aws_eks_access_entry.runner_readonly.principal_arn

  access_scope {
    type = "cluster"
  }
}

and in EC2 Terraform repo:

# eks:DescribeCluster so `aws eks update-kubeconfig` works from the runner. Paired with the
# read-only EKS access entry for this role on test-default-eks (infra-k8s), it lets operators
# kubectl the private cluster via SSM for diagnostics without a hand-built kubeconfig or an ad-hoc grant.
# IAM (this) authorises the DescribeCluster call; the access entry (RBAC) authorises what kubectl can read.
data "aws_iam_policy_document" "eks_describe" {
  statement {
    sid       = "EksDescribeCluster"
    actions   = ["eks:DescribeCluster"]
    resources = ["arn:aws:eks:${var.aws_region}:${data.aws_caller_identity.current.account_id}:cluster/test-default-eks"]
  }
}

resource "aws_iam_role_policy" "eks_describe" {
  name   = "${local.name}-eks-describe"
  role   = aws_iam_role.runner.id
  policy = data.aws_iam_policy_document.eks_describe.json
}




Alternative: Custom Kubernetes Groups


If you don't want to grant full AmazonEKSClusterAdminPolicy and prefer mapping the EC2 instance to custom Kubernetes RBAC roles, you can pass target groups directly via the access entry:


resource "aws_eks_access_entry" "cicd_runner_entry" {
  cluster_name      = "my-cluster-name"
  principal_arn     = aws_iam_role.cicd_runner.arn
  kubernetes_groups = ["my-custom-helm-deployers-group"]
  type              = "STANDARD"
}

(You would then manage a native Kubernetes ClusterRoleBinding pointing to my-custom-helm-deployers-group using the kubernetes provider.)

3. How the EC2 Instance Connects


Once the IAM and EKS access configurations are applied, you don't need to pass raw AWS access keys to the EC2 instance. The tools leverage the Instance Profile automatically.
For Helm & kubectl:Log onto the EC2 instance (or configure your shell script/user-data) to update the local kubeconfig using the AWS CLI:

aws eks update-kubeconfig --region us-east-1 --name my-cluster-name

When Helm or kubectl is invoked, it calls the aws eks get-token command under the hood, uses the EC2's IAM profile to generate a signed token, routes over the private VPC endpoint network, and authenticates flawlessly.

For the Terraform Kubernetes/Helm Providers:


If Terraform itself is running on that EC2 instance and deploying helm charts into EKS, configure your Terraform provider block to pull tokens dynamically using the instance profile credentials:

data "aws_eks_cluster_auth" "cluster" {
  name = "my-cluster-name"
}

provider "helm" {
  kubernetes {
    host                   = "https://ABC123XYZ.gr7.us-east-1.eks.amazonaws.com"
    cluster_ca_certificate = base64decode("CLUSTER_CA_DATA")
    token                  = data.aws_eks_cluster_auth.cluster.token
  }
}


Just-In-Time (JIT) Access (Least Privilege Workflow)


Another approach is a Just-In-Time (JIT) access or Least Privilege workflow. It is highly secure, but it can quickly become an operational headache for a DevOps engineer if it isn't completely automated.

Instead of giving your GitHub Actions runner continuous, permanent admin access to your private EKS cluster, you only grant access for the exact duration of an ad-hoc task, and then immediately rip it away.

Here is exactly how that breakdown works mechanically step-by-step, why someone would build it, and the hidden risks you should look out for.

The Workflow Cycle Breakdown


When you trigger a workflow or step that needs to run an ad-hoc kubectl command, an automation tool (like a pipeline script, a step function, or a privileged security wrapper) executes this lifecycle:

[Pipeline Starts] 
       │
       ▼
1. CREATE ACCESS ENTRY ──► (Allows 'github-runner-role' network identity into EKS)
       │
       ▼
2. ASSOCIATE POLICY    ──► (Binds ClusterAdmin or specific RBAC permissions)
       │
       ▼
3. RUN KUBECTL COMMAND ──► (The runner executes its ad-hoc tasks safely)
       │
       ▼
4. DISASSOCIATE POLICY ──► (Strip away the RBAC permissions)
       │
       ▼
5. DELETE ACCESS ENTRY ──► (Completely remove the IAM role mapping from EKS)
       │
       ▼
[Pipeline Finishes]


1. The Gate: create-access-entry

By default, your runner's IAM role (github-runner-role) is completely blocked at the EKS front door. Even if it can physically route to the API endpoint, EKS will return a 401 Unauthorized.

The setup script calls the AWS API to create an EKS Access Entry. This tells EKS: "Be ready to recognize this specific IAM role ARN."

2. The Permission: associate-access-policy

Creating the entry just gets the runner past the front door; it still has zero privileges inside the cluster. The next API call binds an access policy (like AmazonEKSClusterAdminPolicy or a custom namespace policy) to that entry. Now kubectl commands will actually work.

3. The Execution: Ad-hoc Commands

The runner runs aws eks update-kubeconfig, grabs its temporary token, and runs the necessary kubectl or helm actions.

4. The Clean-up: Revoking Access

Once the kubectl step finishes, the pipeline runs a cleanup block (usually wrapped in a always() or finally clause to ensure it runs even if the deployment fails). It reverses the process by disassociating the policy and deleting the access entry entirely. The runner is now unauthorized again.

Why would someone architecture it this way?


  • Blast Radius Reduction: If that specific GitHub runner instance or the AWS role credentials are ever compromised, the attacker cannot access your Kubernetes cluster because the role has no standing permissions.
  • Audit Trails: Every single time access is granted, used, and revoked, a permanent trail is logged in AWS CloudTrail. It makes passing compliance checks exceptionally easy because you can prove no system has persistent, unchallenged access.

The Catch: Why this pattern can trip you up


While it sounds bulletproof on paper, this setup has a few major operational trade-offs that you have to manage closely:
  • Pipeline Failures Leave Backdoors: If the runner gets abruptly killed (e.g., the GitHub Actions job is manually canceled mid-run, or the runner machine loses power), the cleanup step might never run. The role will remain an admin of your cluster until someone goes in and manually deletes the entry.
  • Race Conditions on Concurrent Runs: If you have multiple workflows trying to use that same runner role at the same time, Run A might delete the access entry while Run B is right in the middle of executing a kubectl apply.
  • AWS API Throttling: If you scale up your CI/CD pipelines and are constantly creating and destroying access entries every few minutes, you can easily hit AWS API rate limits (throttled EKS control plane requests).

AWS EKS Cluster API


In AWS EKS, managing the cluster's API endpoint correctly is one of the most critical steps for balancing day-to-day DevOps convenience with robust security.

Here is exactly how the EKS API works, what public vs. private means, how subnets factor into the equation, and how authentication is handled.

1. What is the Cluster's API?


The cluster API is the Kubernetes API Server control plane hosted and managed by AWS. It is the single entry point for all administrative actions. Every time you run a command via kubectl, deploy a Helm chart, or when worker nodes send heartbeats, they are talking to this API server endpoint. AWS gives you a unique HTTPS URL for this endpoint (e.g., https://ABC123XYZ.gr7.us-east-1.eks.amazonaws.com).  

2. Public vs. Private Endpoints


This setting dictates network-level visibility—essentially, who can physically route traffic to that HTTPS URL. 

EKS supports three access modes:  

  • Public Only (Default): The API URL resolves to a public IP address. Anyone on the internet can physically reach the login page, but they still must authenticate to get in. Worker nodes inside your VPC must route out to the internet (often via a NAT Gateway) to talk back to the control plane.  
  • Public and Private (Recommended for most production setups): The URL resolves to a public IP for external users, but within your VPC, it resolves directly to private IP addresses via internal Cross-Account ENIs (Elastic Network Interfaces). Your worker nodes communicate completely internally, while you can still run kubectl from your local machine without a VPN. You can also allowlist specific CIDRs/IPs on the public side to lock it down to your office or home IP.  
  • Private Only: The public internet endpoint is completely shut off. The API URL resolves only to private IPs inside your VPC. To run a kubectl command, you must be inside the VPC network (e.g., via AWS Client VPN, Transit Gateway, or a bastion host).  

How to change it:


You can switch this at any time with no downtime via the AWS Console (Cluster -> Networking -> Manage Endpoint Access) or via the AWS CLI:  

Example: Switch to Public + Private recommended mode

aws eks update-cluster-config \
  --name my-cluster \
  --resources-vpc-config endpointPublicAccess=true,endpointPrivateAccess=true


3. Does it matter which Subnets the cluster runs on?


Yes, but do not confuse Cluster Subnets with the API Endpoint setting. They are two distinct layers.

When you create an EKS cluster, you must provide a set of subnets (usually at least two in different Availability Zones). EKS drops its managed network interfaces (X-ENIs) into these subnets so the control plane can communicate with your worker nodes. 

  • Best Practice: Always provide private subnets (subnets that route outbound via a NAT gateway and don't assign public IPs to instances) for your actual worker nodes and EKS ENIs. This keeps your EC2 nodes completely hidden from the internet.
  • The Disconnect: You can have an EKS cluster running on entirely private subnets, but still have a Public API endpoint. The API endpoint is hosted by AWS's managed infrastructure, not directly inside your subnets.

4. Does the API require authentication? Who can access it?


Yes, absolutely. Network visibility (Public/Private) is just the perimeter fence. Even if your API endpoint is wide-open public, nobody can do anything without explicit authentication and authorization.

EKS uses a dual-layer system to handle access:

Layer 1: Authentication (Who are you?) via AWS IAM


EKS natively leverages AWS IAM for authentication. When you run kubectl, your local setup uses the AWS CLI to generate a temporary, signed token based on your current AWS IAM identity (User or Role). It passes this token in the HTTPS header to the EKS API. EKS verifies this token with AWS IAM to prove you are who you say you are.

Layer 2: Authorization (What can you do?) via Kubernetes RBAC


Just having a valid IAM identity isn't enough. Once EKS knows your identity, it hands the request off to the internal Kubernetes Role-Based Access Control (RBAC) engine.
  • By default, the IAM identity that originally created the EKS cluster is automatically granted full cluster administrator (system:masters) privileges.
  • For anyone else (other team members, CI/CD tools), you must map their AWS IAM ARN to specific Kubernetes Groups using the EKS Access Entries feature (or the legacy aws-auth ConfigMap) and assign them RBAC Roles/ClusterRoles.

Summary of the flow:


kubectl get pods --> 
Local AWS CLI generates an IAM token -->
Request hits EKS API (routes via public internet or private VPC depending on endpoint settings) -->
EKS checks token validity via IAM -->
Kubernetes RBAC verifies if your user profile has permission to view pods -->
Success

Thursday, 2 July 2026

Mitigating Load Balancer Routing Clamping




In computing and networking, routing clumping (also known as traffic clumping or clustering) refers to the phenomenon where a load balancer or router unevenly sends bursts of traffic to the same backend servers instead of distributing requests evenly across the entire available server pool.

While a load balancer is designed to act as a traffic cop, routing clumping defeats this purpose, momentarily overloading specific servers while leaving others idle.

Primary Causes of Routing Clumping


  • Hashing Bias: Algorithms like 5-tuple hashing (which use source IP, destination IP, ports, and protocols) calculate a fixed value to select a routing path. If thousands of users share a proxy or exit node, the load balancer may calculate the same hash and clump all their traffic onto a single backend server.
  • Sticky Sessions: If you use session persistence (sticky sessions) based on a client's IP, multiple requests from the same office or network range will clump to a single server to maintain session continuity.
  • Connection Multiplexing: Load balancers maintain long-lived backend connections to save resources. When a new batch of requests comes in, the load balancer routes them through existing open connections, causing requests to cluster on specific nodes rather than opening new connections to idle servers.
  • Inconsistent Pod or Instance Count: In environments like Kubernetes, if traffic is processed node-by-node, unequal distribution of application pods across nodes or restrictive node affinities can cause traffic to clump in specific zones or servers.

How to Prevent and Mitigate Clumping


  • Use Round Robin or Least Connections: Switch from IP-hashing or fixed-hash methods to AWS Least Connections or round-robin algorithms, which evaluate server availability in real-time rather than relying on mathematical routing properties.
  • Enable Cross-Zone Load Balancing: For cloud architectures (like AWS Elastic Load Balancing), enabling cross-zone load balancing forces your load balancers to distribute traffic evenly across all registered targets in all enabled Availability Zones.
  • Use Layer 7 Intelligent Routing: Instead of Layer 4 packet forwarding, use a Layer 7 Application Load Balancer to inspect HTTP headers and distribute traffic based on request context, allowing for finer-grained distribution.
  • Deploy Hierarchical Splitting: For massive, hyperscale systems, implementing a hierarchical L4 + L7 structure (ultra-fast L4 hardware balancers routing to a software-based L7 pool) helps prevent clumping while maintaining high raw throughput.


Why round robin routing didn't work well in this scenario so we need to use least connections routing? 



Good question — the answer is about the kind of workload service3 is. Round robin isn't broken in general; it's the wrong fit for this specific traffic shape.

  What each algorithm does 
  • Round robin: hands each new request to the next target in rotation (pod A, B, C, A, B, C…), purely by count — blind to how busy each target currently is.
  • Least outstanding requests (LOR): sends each new request to the target with the fewest in-flight requests right now — i.e., the least-busy one.

Why round robin fails here

Round robin distributes by count, which only equals distributing by load when requests are uniform and short. service3 is the opposite — it has the toxic combination:

1. Highly variable, long service times — a spaCy extraction is anywhere from ~0.3s to multi-second depending on text length/entity count. One heavy request ties a pod up for seconds.
2. Low per-pod concurrency — each pod does ~1–2 concurrent extractions (2 workers, GIL-bound). It saturates almost immediately.

Under those conditions, round robin keeps handing new requests to a pod that's still grinding a heavy multi-second extraction, just because it's "next in rotation" — while a pod that just finished sits idle. The new request then queues head-of-line behind the heavy one for seconds, even though free capacity exists elsewhere. Over a burst this compounds: unlucky pods accumulate a backlog while others drain and idle → exactly the clumping / 33%-CPU-but-3s-latency pattern we measured.

The analogy: round robin is a supermarket sending shoppers to checkout lanes strictly 1‑2‑3‑1‑2‑3, ignoring that lane 2 has someone with a giant cart. You get stuck behind the big cart while lane 3 is empty. LOR is "go to the lane with the fewest items in it right now."

Why LOR fixes it

LOR tracks in-flight requests per target and skips the busy pod — a pod mid-heavy-extraction has a high outstanding count, so new requests go to the idle/draining pods instead. It self-corrects for variance (whichever pod is busy, for any reason, automatically gets fewer new requests until it catches up) and it reaches the idle capacity round robin was stranding.

The catch — why we also needed IP mode

LOR only helps if the ALB is choosing among the right units. In instance mode, the ALB's targets are nodes, so it'd balance outstanding requests across nodes — then kube-proxy randomly picks a pod within the node, re-scrambling the per-pod balance. LOR literally can't see per-pod busyness in instance mode.

IP mode makes the targets pods, so LOR's "least outstanding" is measured per pod → it can actually steer to the least-busy pod. That's why the fix is ip mode + LOR together: ip mode gives LOR the right granularity; LOR gives ip mode the right decision rule. Either alone wouldn't do it.

Two footnotes

- The other 5 services keep round robin — deliberately. auth/data/game/webhook are fast (<0.5s), uniform, high-concurrency — round robin distributes them fine, and there's no reason to touch them. The algorithm choice is workload-specific: LOR for
  slow/variable/expensive backends, round robin for fast/uniform ones.
- Terminology: you said "least connections" — the ALB (L7) equivalent is least outstanding requests, which counts in-flight HTTP requests, not TCP connections. That distinction matters here: the ALB reuses keep-alive connections (one connection carries many sequential requests), so "least connections" would be misleading — LOR counts the actual requests, which is the right signal for this.



In which cases round-robin doesn't help so we need to use least outstanding requests routing?


While Round-Robin works perfectly for uniform traffic and identical servers, it fails when the system experiences variations in request complexity or server capacity. You must switch from Round-Robin to Least Outstanding Requests (LOR) (also known as Least Connections) in the following four specific engineering scenarios:

1. Varying Request Processing Times (Asymmetric Workloads)


  • The Problem: In many applications, some API calls take 5 milliseconds (e.g., fetching a profile cached in memory), while others take 5 seconds (e.g., generating a heavy PDF report or running a complex database query).Why Round-Robin Fails: Round-Robin blindly hands out requests in a strict, alternating sequence. If Server A randomly receives a consecutive string of heavy PDF requests while Server B receives fast cache requests, Server A's queue will spike, causing high latency or timeouts, while Server B sits mostly idle.
  • How LOR Helps: LOR actively tracks the active connection count. It will notice Server A is backed up with pending work and will divert all new incoming traffic to Server B until Server A finishes its heavy processing.

2. Heterogeneous Server Capacities (Mixed Server Sizes)


  • The Problem: Production clusters often use mixed hardware. For example, during an auto-scaling event, you might temporarily mix older 4-core virtual machines with newer, high-performance 16-core instances.Why Round-Robin Fails: Round-Robin treats every backend target as equal. It sends exactly 1,000 requests to the weak 4-core machine and exactly 1,000 requests to the powerful 16-core machine. The weaker machine will quickly choke, run out of memory, or drop packets, while the stronger machine remains underutilised.
  • How LOR Helps: The faster, more powerful server processes and closes connections much quicker than the weaker server. Because its outstanding connection count drops rapidly, LOR naturally funnels a significantly higher volume of traffic to the stronger hardware without needing manual weight configurations.

3. Persistent and Long-Lived Connections (WebSocket & gRPC)


  • The Problem: Modern applications rely heavily on persistent connections like WebSockets, HTTP/2 multiplexing, server-sent events (SSE), or gRPC streams. These connections stay open for minutes or hours.Why Round-Robin Fails: Round-Robin only counts the initial connection establishment. If Server A hosts 50 clients who stay connected for 3 hours chatting, and Server B hosts 50 clients who disconnect after 30 seconds, Round-Robin will continue to feed new connections to both servers equally. Server A will slowly crush under the cumulative weight of long-lived active sessions.
  • How LOR Helps: LOR continuously monitors active, open connections rather than connection arrival rates. It will see that Server A has 50 active outstanding connections and Server B has 0, immediately routing all new clients to Server B.

4. Unpredictable Backend "Cold Starts" and Drifts


The Problem: When a new application container or server boots up (e.g., during a Kubernetes deployment or AWS auto-scaling event), it often suffers from a "cold start" where it runs slowly while warming up caches or compiling code just-in-time (JIT).Why Round-Robin Fails: Round-Robin immediately floods the newly booted server with its full share of production traffic. Because the server is not yet performing at 100% capacity, this sudden burst easily overwhelms it, causing it to crash immediately after launching.
How LOR Helps: LOR naturally throttles traffic to the warming server. Since the cold server processes its first few requests slowly, its outstanding request count will naturally rise, signaling the load balancer to back off and route traffic elsewhere until the server catches up.


Algorithm Comparison Table


  • Scenario
  • Round-Robin Behavior
  • Least Outstanding Requests Behavior
  • All requests take equal time 
  • Perfect, completely equal distribution.
  • Excellent, identical result to Round-Robin.
  • Mix of short & long requests
  • Overloads random servers (creates traffic clumps).
  • Dynamically balances the processing queue.
  • Mixed server capacities
  • Overwhelms smaller, weaker backend servers.
  • Directs more traffic to faster instances automatically.
  • Long-lived WebSockets / gRPC
  • Ignores session duration; causes massive load imbalance.
  • Tracks real-time active sessions perfectly.

If you are using a cloud platform, you can learn more about implementing this via the AWS Least Outstanding Requests Documentation or the NGINX Least Connections Guide.



k8s ingress - AWS ALB


To implement Least Outstanding Requests (LOR) routing with an AWS Application Load Balancer (ALB) inside a Kubernetes cluster, you must configure it using the AWS Load Balancer Controller.

By default, the AWS ALB uses Round-Robin routing. You can change this behavior by applying a specific routing annotation to your Kubernetes TargetGroupBinding or Ingress resource.


1. Ingress Configuration Example


Apply the alb.ingress.kubernetes.io/target-group-attributes annotation to your Ingress manifest. This tells the AWS controller to configure the underlying target groups to use the load_balancing.algorithm.type=least_outstanding_requests setting.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  namespace: production
  annotations:
    # Essential ALB Controller configurations
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    
    # Enable Least Outstanding Requests Routing
    alb.ingress.kubernetes.io/target-group-attributes: load_balancing.algorithm.type=least_outstanding_requests
spec:
  rules:
    - http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app-service
                port:
                  number: 80



2. Crucial Step: Use "IP" Target Type


When running AWS ALB in Kubernetes, you should always set alb.ingress.kubernetes.io/target-type: ip instead of instance mode. 

  • Why? In instance mode, the ALB routes traffic to NodePorts on the EC2 worker nodes. The node then uses kube-proxy (iptables or IPVS) to randomly route the packet to a pod. This completely breaks the math behind Least Outstanding Requests because the ALB can only see outstanding connections to the node, not the actual pods.
  • The Fix: Using ip mode configures the ALB to bypass the node network entirely and route traffic directly to the Pod IPs. This gives the ALB true visibility into the exact outstanding request count for each application container.

3. Verify the Configuration


After applying the manifest via kubectl apply -f ingress.yaml, you can verify the changes are active in AWS:
  • Open the AWS EC2 Console and navigate to Target Groups.
  • Select the target group automatically generated by your Kubernetes Ingress.
  • Click on the Attributes tab.
  • Verify that Routing algorithm is set to Least outstanding requests.

Alternatively, check the AWS Load Balancer Controller logs to ensure the modification was successfully reconciled:

kubectl logs -n kube-system deployment/aws-load-balancer-controller

---

Wednesday, 1 July 2026

Introduction to Velero

 


Velero is an open-source disaster recovery, Kubernetes-native backup, restore and migration tool for Kubernetes. It allows you to back up and restore both your Kubernetes cluster resources and, optionally, the persistent volumes (PVs) that hold application data.

It was originally created by Heptio (the company founded by two of Kubernetes' creators) and is now maintained by VMware and the open-source community.

What does Velero back up?

Velero can back up:

  • Kubernetes resources (Cluster API objects):
    • Deployments
    • StatefulSets
    • Services
    • Ingresses
    • ConfigMaps
    • Secrets
    • CRDs
    • Namespaces
    • RBAC resources
    • Custom Resources
They are stored as tarballs in an S3 bucket.

Optionally, it can also back up:

  • Persistent Volumes (application data)
    • via storage snapshots (AWS EBS, Azure Disk, GCP Persistent Disk, etc.)
      • cloud snapshots (EBS snapshots through the CSI driver)
    • or via a file-level backup tool called Node Agent (formerly Restic)
      • file-level backup with the built-in Kopia/Restic uploader for non-snapshottable volumes (EFS, hostPath, etc.)

How it works

A typical Velero deployment consists of:


                    +----------------+
| Kubernetes API |
+--------+-------+
|
Velero Server
|
+-------------------+-------------------+
| |
Metadata Backup Volume Backup
| |
v v
Object Storage Snapshot or File Backup
(S3, Azure Blob, (EBS, CSI Snapshot,
GCS, MinIO...) Node Agent/Restic)


For example:

  • Cluster metadata → stored in an S3 bucket
  • PV data → stored as EBS snapshots or uploaded to object storage

Typical use cases

1. Disaster recovery

Your EKS cluster is accidentally deleted.

With Velero you can:

  • recreate the cluster
  • install Velero
  • restore all workloads
  • restore persistent data

2. Accidental deletion

Someone runs:

kubectl delete namespace production

Instead of recreating everything manually:

velero restore create \
--from-backup production-backup

3. Cluster migration

Move workloads from:

  • EKS → EKS
  • EKS → AKS
  • EKS → GKE
  • On-prem → cloud

Velero restores Kubernetes objects into the new cluster.


4. Scheduled backups

Example:

Every night at 2 AM



Backup namespaces:
- production
- monitoring
- logging

Retention can be configured, for example:

Keep 30 daily backups
Delete older ones automatically

What it does NOT back up

Velero does not automatically back up:

  • etcd directly
  • cloud infrastructure (VPCs, Load Balancers, IAM, Security Groups)
  • managed databases like RDS
  • container images (they remain in your registry)
  • external services

Those require separate backup strategies.


Storage providers

Velero supports many object storage backends:

  • Amazon S3
  • MinIO
  • Azure Blob Storage
  • Google Cloud Storage
  • OCI Object Storage
  • many S3-compatible systems

Persistent Volume backup methods

There are two main approaches.

1. CSI snapshots (preferred)

If your storage class supports the Container Storage Interface (CSI) snapshot API:

PVC

VolumeSnapshot

Cloud snapshot

Advantages:

  • very fast
  • incremental (depending on storage backend)
  • cloud-native
  • recommended

2. Node Agent (formerly Restic)

If snapshots aren't available:

PVC

Read filesystem

Compress

Upload to object storage

Advantages:

  • works almost everywhere
  • storage-independent

Disadvantages:

  • slower
  • consumes CPU and network bandwidth

Example architecture in AWS

                 Amazon EKS
|
+-----------+-----------+
| |
Kubernetes API Persistent Volumes
| |
Velero Server EBS Snapshots
|
|
S3 Bucket
backups/

Example installation

Install the CLI:

brew install velero

Velero CLI operates on the current kubectl context by default. It automatically inherits your active cluster credentials and configurations from your default kubeconfig file. You can override this behavior using the --kubecontext or --kubeconfig flags

Deploy into an EKS cluster:

velero install \
--provider aws \
--plugins velero/velero-plugin-for-aws \
--bucket my-backups \
--backup-location-config region=eu-west-2

Example backup

Back up an entire cluster:

velero backup create full-cluster

Back up a namespace:

velero backup create production \
--include-namespaces production

Example restore

velero restore create \
--from-backup full-cluster

Scheduling

Create a nightly backup:

velero schedule create nightly \
--schedule="0 2 * * *"

When should you use Velero?

Velero is a good fit if you want to:

  • Recover Kubernetes workloads after accidental deletion or cluster failure.
  • Back up application manifests and, optionally, persistent data.
  • Migrate workloads between Kubernetes clusters or cloud providers.
  • Automate recurring backups with retention policies.
  • Protect stateful applications running on Kubernetes.

If your applications also depend on external systems (for example, managed databases, message brokers, or cloud resources), Velero should be part of a broader disaster recovery strategy rather than the only backup solution.

Velero vs. etcd backup

FeatureVeleroetcd backup
Kubernetes resources
Persistent volume data
Restore individual namespaces
Restore individual applications
Migrate between clustersLimited
Cloud agnosticMostly
Disaster recovery for applicationsPartial

For managed Kubernetes services such as Amazon Elastic Kubernetes Service (EKS), Velero is generally the preferred backup tool because it focuses on application-level recovery rather than restoring the control plane itself. In contrast, direct etcd backups are more common in self-managed Kubernetes clusters where you control the control plane.

Tuesday, 21 April 2026

Provisioning AWS EKS Cluster with terraform-aws-modules/eks/aws





In this article we want to explore and breakdown its key components and their purposes.

We'd typically use this module like here:

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "21.15.1"
  ...
}


Let's explore this module's attributes.

1. Cluster Configuration


name,  version

Sets the name and Kubernetes version for the EKS cluster. Use local and variable values for flexibility.

endpoint_public_access

Set public (Internet) access to the Kubernetes API endpoint (via kubectl). Disable it for enhanced security. 

endpoint_private_access

Set private access to the API endpoint, whether only resources within the VPC can access it. If enabled, it is only reachable from within the VPC (Virtual Private Cloud) where your EKS cluster is deployed. There are few ways to access it:

How to Access the Kubernetes API from VPC

1. Use a Bastion Host or EC2 Instance in the VPC

Launch an EC2 instance (bastion host or jump box) in a subnet within the same VPC as your EKS cluster.
SSH into this instance, and from there, use kubectl to access the cluster.
Alternatively, use SSH port forwarding or a VPN to proxy kubectl commands from your local machine through the bastion.

2. Use AWS Systems Manager (SSM) Session Manager

If your EC2 instances have the SSM agent and the necessary IAM permissions, you can use AWS SSM Session Manager to start a shell session on an instance in the VPC, then run kubectl from there.

3. Use a VPN Connection

Set up a VPN (such as AWS Client VPN or OpenVPN, or Site-to-site VPN for office LAN) that connects your local network to the VPC. Once connected, your local machine will be able to reach the private endpoint.

4. Use AWS PrivateLink (Interface VPC Endpoints)

For advanced scenarios, you can use AWS PrivateLink to expose the Kubernetes API endpoint privately to other VPCs or on-premises networks.


enable_cluster_creator_admin_permissions


If enabled, grants admin permissions to the user who creates the cluster.


2. Logging and Add-ons


enabled_log_types

Enables logging for various Kubernetes components (API, audit, authenticator, controllerManager, scheduler) for monitoring and troubleshooting.

Example:

  enabled_log_types = [
    "api",
    "audit",
    "authenticator",
    "controllerManager",
    "scheduler"
  ]

addons

A dictionary-type attribute which installs and configures essential Kubernetes add-ons. Dictionary keys are addon names like:
  • coredns
  • kube-proxy
  • aws-ebs-csi-driver
  • vpc-cni

Dictionary values are objects which attributes are:
  • most_recent - to set using the latest version (set it to false for version pinning)
  • version - addon version (use it for version pinning)
  • before_compute - set it to true if addon should be installed and set before nodes (compute layer)
  • service_account_role_arn - to configure addon with IAM roles for service accounts, enabling secure integration with AWS services.

Example:

addons = {
    ...
    vpc-cni = {
      most_recent              = false
      version                  = "v1.21.1-eksbuild.7"
      before_compute           = true
      service_account_role_arn = module.k8s_default_vpc_cni_irsa.iam_role_arn
    }
    ...
}

VPC CNI (Container networking interface) is responsible for allocating IP addresses to the Kubernetes nodes and provides networking to pods. The plugin manage network interfaces (ENIs) on the nodes and uses it to assign IP addresses to pods.



3. Networking

We need to integrate the EKS cluster with existing VPC and subnets:

vpc_id 

VPC ID

subnet_ids

Subnets in which nodes (EC2 instances) will be created.
Where your worker nodes (EC2 instances) run.

control_plane_subnet_ids

Where the EKS control plane ENIs (network interfaces) are placed
Defines where the EKS control plane creates its Elastic Network Interfaces (ENIs)

What it controls:
  • The EKS control plane runs in an AWS-managed VPC (you don't see it)
  • To communicate with your worker nodes, it creates ENIs in your VPC
  • These ENIs are placed in the subnets you specify here

Typical configuration:

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  
  name = "my-cluster"
  
  # Control plane ENIs go here
  control_plane_subnet_ids = [
    "subnet-private-1a",
    "subnet-private-1b",
    "subnet-private-1c"
  ]
}

Best practices:
  • Usually private subnets
  • Should span multiple AZs for high availability (AWS requires at least 2)
  • Minimum of 2 subnets, maximum of 16
  • Each subnet needs at least 5 available IP addresses

What these ENIs do:
  • Allow the control plane to communicate with worker nodes
  • Allow worker nodes to communicate with the API server
  • Handle API server endpoint traffic


security_group_additional_rules


Adds custom security group rules for the cluster, such as allowing node-to-node communication and VPN access for kubectl.

node_security_group_additional_rules


Further customizes node security groups, allowing all node-to-node traffic and all outbound traffic.



Understanding EKS Architecture

An EKS cluster has two main components:

┌─────────────────────────────────────────────────────────┐
│                    EKS Cluster                          │
│                                                         │
│  ┌───────────────────────────────────────┐              │
│  │   Control Plane (AWS Managed)         │              │
│  │   - API Server                        │              │
│  │   - etcd                              │              │
│  │   - Scheduler                         │              │
│  │   - Controller Manager                │              │
│  │                                       │              │
│  │   Runs in AWS-managed account         │              │
│  └──────────────┬────────────────────────┘              │
│                 │                                       │
│                 │ ENIs in your VPC                      │
│                 │ (control_plane_subnet_ids)            │
│  ┌──────────────▼────────────────────────┐              │
│  │   Your VPC                            │              │
│  │   ┌─────────────────────────────┐     │              │
│  │   │  Worker Nodes (subnet_ids)  │     │              │
│  │   │  - EC2 instances            │     │              │
│  │   │  - Your pods run here       │     │              │
│  │   └─────────────────────────────┘     │              │
│  └───────────────────────────────────────┘              │
└─────────────────────────────────────────────────────────┘

ENI: elastic network interface. It is a logical networking component in a VPC that represents a virtual network card.



4. Node Group Configuration


node_security_group_tags


Adds a tag for Karpenter (an open-source Kubernetes node autoscaler) discovery.

eks_managed_node_group_defaults


Sets default properties for all managed node groups, including:
  • Attaching the CNI policy for networking.
  • Using a specific SSH key.
  • Associating additional security groups.
  • Defining block device mappings for EBS volumes.
  • Attaching the AmazonSSMManagedInstanceCore policy for SSM access.

eks_managed_node_groups


Defines a default managed node group with:
  • A specific AMI type.
  • Desired, minimum, and maximum node counts.
  • Instance types from a variable.
  • On-demand capacity, EBS optimization, and disk size.
  • Custom labels for node identification and environment.

The gold standard for production environments is explicit pinning. This ensures that our infrastructure only changes when we decide to change the code. In order to pin AMI version used in node groups we need to set two attributes:
  • ami_release_version needs to be set. This prevents nodes from cycling unexpectedly during a routine deployment.
  • use_latest_ami_release_version needs to be set to false (without this, terraform plan will still show that it wants to upgrade AMI version, even if we've set ami_release_version)

Example:

  eks_managed_node_groups = {
    "${local.cluster_name}-v1_33" = {
      ...
      ami_release_version            = "1.33.8-20260224"
      use_latest_ami_release_version = false
      ...


5. Tagging


tags


Applies custom tags to all AWS resources created by the module, supporting cost allocation and resource management.


Summary



Our configuration sets up a secure, private, and production-ready EKS cluster with managed node groups, essential add-ons, robust logging, and fine-grained network and IAM controls. It leverages best practices for security (private endpoints, IAM roles for service accounts), scalability (managed node groups, Karpenter tags), and maintainability (modular, versioned, and tagged infrastructure).


---