Skip to content

EKS Auto and Karpenter with VPC secondary IP

EKS Auto mode karpenter EKS Auto mode EKS logo

AWS VPC supports secondary CIDR range that can be assigned to EKS pods. This means that we can have a smaller routable CIDR range for nodes while having a secondary IP range for pods networking.

In order to provision pods in secondary IP range with EKS-Auto, you must create/update following

  • Install/Update VPC-CNI package with custom networking enabled
  • Create ENI config for each subnets in each zone
  • Custom Karpenter NodeClass and NodePool with higher priority than default nodepool

Source Code:

Complete setup

In thins example, I will create a VPC with Primary CIDR range of 10.0.0.0/24 spready across 3 AZ and subnets. Will create secondary CIDR in range of 100.0.0.0/16 **Applying the code **

terraform init -upgrade
terraform apply
# Once EKS is ready, update config
aws eks --profile labs  --region eu-west-1 update-kubeconfig --name eks-auto-demo
kubectl cluster-info
# Apply new custom nodepool
kubectl apply -f Bootstrap/cni-nodepool.yaml

# Create a pod and validate they are assigned with secondary IP
kubectl run nginx --image=nginx

Step 1. Create VPC

Create VPC as normal with necessary CIDR range. VPC module does support secondary IP, however I am creating it separate so that custom tags can be added.

Step 2. Define secondary IP range

# Create secondary CIDR once VPC is created
resource "aws_vpc_ipv4_cidr_block_association" "secondary_cidr" {
  vpc_id     = module.vpc.vpc_id
  cidr_block = local.secondary_cidr_block
}


resource "aws_subnet" "podnet" {
  for_each = {
    for idx, az in local.azs :
    az => {
      cidr_block = local.pod_subnets[idx]
    }
  }
  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" # Tag for internal LoadBalancer
    "kubernetes.io/role/cni"          = "1"
    "pod_subnet"                      = "true"
    "subnet_purpose"                  = "EKS_Cluster"
  }
  depends_on = [aws_vpc_ipv4_cidr_block_association.secondary_cidr] # Ensure VPC CIDR is created before subnets
}


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 3. Create EKS-Auto cluster

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "21.0.6"

  name               = "eks-auto-demo"
  kubernetes_version = "1.33"

  # Optional
  endpoint_public_access = true

  # Optional: Adds the current caller identity as an administrator via cluster access entry
  enable_cluster_creator_admin_permissions = true

  compute_config = {
    enabled    = true
    node_pools = ["general-purpose"]
  }

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

  tags = {
    Environment = "dev"
    Terraform   = "true"
    Owner       = "D Vettom"
  }
}

Step 4. Instll VPC-CNI and create ENI config per pod subnet

Must enable custom networking, and prefix deligation. Also update configs for creating ENI config per pods subnet/AZ

 locals {
  az        = [for k, v in aws_subnet.podnet : v.availability_zone]
  subnet_id = [for k, v in aws_subnet.podnet : v.id]
}

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.6"
  values = [
    <<-EOT
    env:
        AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG: "true"
        ENABLE_POD_ENI: "true"
        ENABLE_PREFIX_DELEGATION: "true"
        ENABLE_SUBNET_DISCOVERY: "true"  
    eniConfig:
        # Specifies whether ENIConfigs should be created
        create: true
        region: "${data.aws_region.current.region}"
        subnets:
            "${local.az[0]}":
                id: "${local.subnet_id[0]}"
                securityGroups:
                    - "${module.eks.node_security_group_id}"
            "${local.az[1]}":
                id: "${local.subnet_id[1]}"
                securityGroups:
                    - "${module.eks.node_security_group_id}"
            "${local.az[2]}":
                id: "${local.subnet_id[2]}"
                securityGroups:
                    - "${module.eks.node_security_group_id}"                                        
    EOT
  ]
}

Step 5. (Optional) Create SG for Pods network

You can create custom security group or use same as Clusters Security group

 resource "aws_security_group" "pods_sg" {
  name        = "eks-auto-secondary-pods-sg"
  description = "Security group for EKS Auto Pods using secondary IP address"
  vpc_id      = module.vpc.vpc_id
  tags = {
    Name               = "eks-auto-secondary-pods-sg"
    pod_security_group = "true"
    purpose            = "SG for Pods using secondary IP address"
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8", "100.0.0.0/8"]
  }
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8", "100.0.0.0/8"]
  }
  ingress {
    from_port   = 1025
    to_port     = 65535
    protocol    = "tcp"
    cidr_blocks = ["100.0.0.0/8"]
  }
  ingress {
    from_port   = 53
    to_port     = 53
    protocol    = "udp"
    cidr_blocks = ["100.0.0.0/8"]
  }
}

resource "aws_vpc_security_group_egress_rule" "allow_all_traffic_ipv4" {
  security_group_id = aws_security_group.pods_sg.id
  description       = "Allow all outbound traffic"
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1" # semantically equivalent to all ports
}

Step 6. Apply custom Karpenter Nodepool config

Create a custom nodeClass with securityGroupSelectorTerms and podSecurityGroupSelectorTerms. Both can be same but ideal use dedicated. Update NodeClass with podSecurityGroupSelectorTerms and podSubnetSelectorTerms

Nodepool must refer to custom nodeClass and have a weight of more than 0 to take precedence over default nodepool provisioned by EKS-Auto

NodeClass must include securityGroupSelectorTerms, podSecurityGroupSelectorTerms, subnetSelectorTerms and podSubnetSelectorTerms. You must also ensure that all the subnets selected for EKS node must have corresponding podsubnet and ENI configuration available.

apiVersion: eks.amazonaws.com/v1
kind: NodeClass
metadata:
  finalizers:
  - eks.amazonaws.com/termination
  name: primary-nodeclass
spec:
  ephemeralStorage:
    iops: 3000
    size: 80Gi
    throughput: 125
  networkPolicy: DefaultAllow
  networkPolicyEventLogs: Disabled
  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.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

Test pods IP

Deploy a pond and verify that node IP is from private subnet and pods IP from pods subnet/secondary range

kubectl run nginx --image=nginx