Skip to content

EKS Auto and Karpenter with VPC Secondary IP

EKS Auto mode karpenter ENI Terraform logo

In large enterprise networks, allocating thousands of routable IPv4 addresses to Kubernetes pods can quickly exhaust the corporate VPC IP space.

AWS VPC supports adding Secondary IPv4 CIDR blocks (e.g. 100.64.0.0/16 / RFC 6598 carrier-grade NAT or 100.x.x.x). By configuring VPC-CNI Custom Networking and custom Karpenter NodeClasses, worker nodes consume a small, routable primary CIDR while all pods receive IPs from the large secondary CIDR range.

📁 Source Code: aws-eks-terraform / EKS-Auto-secondary


📺 Video Walkthrough


Architecture Overview

graph TD
    subgraph VPC ["VPC (Primary CIDR: 10.0.0.0/24 & Secondary CIDR: 100.64.0.0/16)"]
        subgraph NodeSubnet ["Node Subnets (Primary: 10.0.0.x)"]
            Node["EKS Node (Primary ENI: 10.0.0.15)"]
        end
        subgraph PodSubnet ["Pod Subnets (Secondary: 100.64.x.x)"]
            SecondaryENI["Secondary ENI (100.64.1.20)"]
            PodA["Pod A (100.64.1.21)"]
            PodB["Pod B (100.64.1.22)"]
        end
    end
    Node --> SecondaryENI
    SecondaryENI --> PodA
    SecondaryENI --> PodB

Step-by-Step Implementation

Step 1. Define Secondary CIDR Block and Pod Subnets

Attach a secondary CIDR to the VPC and provision subnets across Availability Zones:

secondary-cidr.tf

resource "aws_vpc_ipv4_cidr_block_association" "secondary_cidr" {
  vpc_id     = module.vpc.vpc_id
  cidr_block = "100.64.0.0/16"
}

resource "aws_subnet" "podnet" {
  for_each = {
    for idx, az in local.azs :
    az => {
      cidr_block = local.pod_subnets[idx] # e.g. ["100.64.0.0/19", "100.64.32.0/19", "100.64.64.0/19"]
    }
  }

  vpc_id            = module.vpc.vpc_id
  availability_zone = each.key
  cidr_block        = each.value.cidr_block

  tags = {
    "Name"                            = "${module.vpc.name}-pod-subnet-${each.key}"
    "kubernetes.io/role/internal-elb" = "1"
    "kubernetes.io/role/cni"          = "1"
    "pod_subnet"                      = "true"
    "subnet_purpose"                  = "EKS_Cluster"
  }

  depends_on = [aws_vpc_ipv4_cidr_block_association.secondary_cidr]
}

resource "aws_route_table_association" "podnet_association" {
  for_each       = aws_subnet.podnet
  subnet_id      = each.value.id
  route_table_id = module.vpc.private_route_table_ids[0]
}


Step 2. Configure VPC-CNI with Custom Networking & ENIConfigs

Enable AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG in VPC-CNI and generate an ENIConfig resource for each Availability Zone:

cni.tf

resource "helm_release" "vpc_cni" {
  name       = "aws-vpc-cni"
  namespace  = "kube-system"
  repository = "https://aws.github.io/eks-charts"
  chart      = "aws-vpc-cni"
  version    = "1.19.0"

  values = [
    <<-EOT
    env:
      AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG: "true"
      ENABLE_POD_ENI: "true"
      ENABLE_PREFIX_DELEGATION: "true"
      ENABLE_SUBNET_DISCOVERY: "true"
    eniConfig:
      create: true
      region: "${data.aws_region.current.region}"
      subnets:
        "${local.azs[0]}":
          id: "${aws_subnet.podnet[local.azs[0]].id}"
          securityGroups:
            - "${module.eks.node_security_group_id}"
        "${local.azs[1]}":
          id: "${aws_subnet.podnet[local.azs[1]].id}"
          securityGroups:
            - "${module.eks.node_security_group_id}"
    EOT
  ]
}


Step 3. Create Karpenter NodeClass with Pod Subnet Selectors

Configure the NodeClass with podSubnetSelectorTerms to bind nodes to the secondary CIDR pod subnets:

custom-nodeclass-nodepool.yaml

apiVersion: eks.amazonaws.com/v1
kind: NodeClass
metadata:
  name: primary-nodeclass
spec:
  ephemeralStorage:
    iops: 3000
    size: 80Gi
    throughput: 125
  networkPolicy: DefaultAllow
  role: AmazonEKSAutoNodeRole
  securityGroupSelectorTerms:
    - tags:
        "Name": "eks-auto-demo-node"
  podSecurityGroupSelectorTerms:
    - tags:
        "Name": "eks-auto-demo-node"
  snatPolicy: Random
  subnetSelectorTerms:
    - tags:
        subnet_type: "private"
        subnet_purpose: "EKS_Cluster"
  podSubnetSelectorTerms:
    - tags:
        pod_subnet: "true"
        subnet_purpose: "EKS_Cluster"
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: primary-nodepool
spec:
  weight: 20
  template:
    spec:
      expireAfter: 336h
      nodeClassRef:
        group: eks.amazonaws.com
        kind: NodeClass
        name: primary-nodeclass
      requirements:
        - key: "karpenter.k8s.aws/instance-hypervisor"
          operator: In
          values: ["nitro"]
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]
        - key: eks.amazonaws.com/instance-category
          operator: In
          values: ["c", "m", "r"]
        - key: eks.amazonaws.com/instance-generation
          operator: Gt
          values: ["5"]
        - key: kubernetes.io/arch
          operator: In
          values:
            - amd64
        - key: kubernetes.io/os
          operator: In
          values:
            - linux
      terminationGracePeriod: 24h0m0s


Verification

# 1. Deploy test pod
kubectl run nginx --image=nginx

# 2. Check pod and node IP addresses
kubectl get pod nginx -o wide

Observe that the worker node's IP address belongs to the primary subnet CIDR (10.0.0.x), while the pod's IP address is assigned from the secondary subnet CIDR (100.64.x.x).