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, 16 July 2026

Helm and Infrastructure

 


Does Helm provision aws infrastructure?



The short answer is no, Helm does not natively provision AWS cloud infrastructure.

Helm is a package manager designed specifically for Kubernetes. Its job is to template, package, and deploy Kubernetes-native YAML manifests (like Deployments, Services, ConfigMaps, and Ingresses) onto an already-running cluster. It does not natively understand or manage AWS resources like VPCs, EC2 instances, RDS databases, or S3 buckets.  However, because the cloud native ecosystem is highly integrated, the line can sometimes look blurry. Here is how that boundary gets crossed and how the two worlds interact.

The "Grey Area": When Helm Indirectly Triggers AWS Provisioning

While Helm itself only talks to the Kubernetes API, certain Kubernetes operators and controllers can watch for Helm-deployed resources and provision AWS infrastructure in response:

1. Cloud-Provider Load Balancers

If your Helm chart deploys a Kubernetes Service of type: LoadBalancer:
  • Helm simply submits that Service YAML to the EKS control plane.
  • The cloud-controller-manager inside EKS sees this and automatically provisions an AWS Classic Load Balancer or Network Load Balancer (NLB) in your AWS account.
  • Similarly, deploying an Ingress resource alongside the AWS Load Balancer Controller (often installed via Helm itself) triggers the creation of an AWS Application Load Balancer (ALB).  

2. Custom Resource Definitions (CRDs) & Operators

You can use Kubernetes operators that are explicitly designed to manage cloud infrastructure through Kubernetes. If you install these operators (often using Helm), you can then use Helm to deploy manifests that provision AWS resources:
  • AWS Controllers for Kubernetes (ACK): This framework allows you to define AWS resources (like an S3 bucket or an RDS instance) directly as Kubernetes custom resources. If you bundle an ACK custom resource inside a Helm chart, running helm install will tell the ACK operator to provision that infrastructure in AWS.
  • Crossplane: Similar to ACK, Crossplane turns your cluster into a control plane. You can package Crossplane claims inside Helm charts to spin up databases, networks, and queues.


The Standard Best Practice: "Separation of Concerns"

Because managing cloud infrastructure via Kubernetes manifests can get complex quickly, the industry-standard pattern is to keep these layers strictly separated:


┌────────────────────────────────────────────────────────┐
│                   App Layer (Helm)                     │
│  - Deployments, Services, Ingress, ConfigMaps, etc.    |
└───────────────────────────┬────────────────────────────┘
                            │ Runs inside
┌───────────────────────────▼────────────────────────────┐
│                Kubernetes Cluster (EKS)                │
└───────────────────────────┬────────────────────────────┘
                            │ Provisioned by
┌───────────────────────────▼────────────────────────────┐
│               Infra Layer (IaC - Terraform)            │
│  - VPC, Subnets, EKS Cluster, IAM, RDS, S3, etc.       │
└────────────────────────────────────────────────────────┘



  • Infrastructure as Code (Terraform, OpenTofu, CloudFormation, Pulumi): Used to provision the underlying AWS infrastructure—the VPC, subnets, IAM roles, EKS cluster, and managed node groups.
  • Helm: Used once the cluster is up and running to deploy and configure applications, ingress controllers, logging agents, and service meshes on top of that infrastructure.

Note: If you want a single tool to bridge the gap, Terraform actually has a Helm Provider. This allows you to use Terraform to provision your EKS cluster first, and then immediately call Helm charts within the same Terraform code to bootstrap your cluster applications.


How to use Terraform to install a Helm chart?


To bridge the gap between infrastructure and application deployment, you can use the HashiCorp Helm Provider directly inside your Terraform configuration.

This approach ensures strict ordering: Terraform first provisions the AWS network and EKS cluster, and only after the cluster is fully active does it initialize Helm to install your charts.

Here is a clean, production-ready example showing how to connect your AWS provider, configure the Kubernetes/Helm providers dynamically using the cluster's output token, and deploy a chart.


# 1. PROVIDERS CONFIGURATION
# We configure Kubernetes and Helm providers dynamically using tokens 
# generated from the newly created AWS EKS cluster.

provider "aws" {
  region = "eu-west-2" # London
}

provider "kubernetes" {
  host                   = aws_eks_cluster.main.endpoint
  cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data)
  token                  = data.aws_eks_cluster_auth.main.token
}

provider "helm" {
  kubernetes {
    host                   = aws_eks_cluster.main.endpoint
    cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data)
    token                  = data.aws_eks_cluster_auth.main.token
  }
}

# Data source to fetch authentication tokens for EKS dynamically
data "aws_eks_cluster_auth" "main" {
  name = aws_eks_cluster.main.name
}

# 2. AWS EKS CLUSTER PROVISIONING (Simplified)
resource "aws_eks_cluster" "main" {
  name     = "production-core-cluster"
  role_arn = aws_iam_role.eks_cluster.arn

  vpc_config {
    subnet_ids = ["subnet-12345678", "subnet-87654321"] # Replace with your VPC subnet IDs
  }
}

# 3. HELM RELEASE PROVISIONING
# Terraform treats this Helm chart as a resource managed within its state file.

resource "helm_release" "ingress_nginx" {
  name             = "ingress-nginx"
  repository       = "https://kubernetes.github.io/ingress-nginx"
  chart            = "ingress-nginx"
  version          = "4.10.0"
  namespace        = "ingress-system"
  create_namespace = true

  # Explicit dependency guarantees the cluster exists before Helm tries to talk to it
  depends_on = [aws_eks_cluster.main]

  # Passing configuration values into the Helm chart
  set {
    name  = "controller.replicaCount"
    value = "3"
  }

  set {
    name  = "controller.service.annotations.service\\.beta\\.kubernetes\\.io/aws-load-balancer-type"
    value = "nlb"
  }
}

Why this pattern works so well:
  • Token-Based Auth Lifecycle: By using the aws_eks_cluster_auth data source, Terraform fetches a short-lived administrative token on the fly during the apply phase, ensuring Helm can authenticate without storing static kubeconfig files on disk or in version control.
  • Escaping Helm Keys: Note the double backslash (\\) in the annotation key. Helm uses dot notation to traverse nested values, so if an AWS annotation key contains a dot (like aws-load-balancer-type), you must escape it in Terraform so Helm interprets it as a single key string rather than a nested map.
  • State Management: When you run terraform destroy, Terraform is smart enough to reverse the order: it calls Helm to cleanly uninstall the charts, deletes any cloud-provider resources (like NLBs) created by those charts, and then tears down the EKS cluster.



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.