Skip to content

Analyzing IP Address Allocation for EKS Auto Mode Clusters

6 minute read
Content level: Advanced
0

This article presents a Python script that helps you analyze IP address utilization across worker nodes and pods in your Amazon EKS cluster, enabling you to proactively manage subnet capacity and prevent IP exhaustion.

Understanding EKS Auto Mode Networking

EKS Auto Mode extends AWS management beyond the Kubernetes control plane to fully automate cluster infrastructure—including compute, networking, storage, and security. Unlike traditional EKS deployments where you configure Managed Node Groups or Auto Scaling groups, EKS Auto Mode handles day-to-day operations and infrastructure decisions on your behalf.

A key architectural change in EKS Auto Mode is how pod IP addresses are assigned. Rather than using the AWS VPC CNI plugin, EKS Auto Mode manages pod IP assignment directly. While this removes the operational burden of CNI configuration, you remain responsible for ensuring your subnets have sufficient IP address capacity for your workloads.

IP Address Allocation Mechanics

By default, EKS Auto Mode assigns a /28 IPv4 prefix to the primary Elastic Network Interface (ENI) of each worker node, drawn from the subnet's CIDR range where the node resides. This allocation provides up to 16 IP addresses per prefix, subject to the instance type's pod density limits. When a node exhausts its initial prefix allocation, EKS Auto Mode dynamically assigns additional /28 prefixes, provided the subnet has available capacity.

Planning for Scale

As your cluster scales, the original subnet allocation may become insufficient for pod IP requirements. Consider these architectural patterns to prevent IP exhaustion:

  • Secondary CIDR ranges: Dedicate separate CIDR blocks exclusively for pod networking
  • Subnet segmentation: Separate subnets for worker nodes and pod IP allocation

Solution Overview

The provided Python script analyzes IP address capacity across your EKS cluster by:

  • Retrieving node and pod information from the Kubernetes API
  • Correlating Kubernetes resources with EC2 instance metadata
  • Examining ENI configurations and IPv4 prefix assignments
  • Calculating per-ENI IP utilization—distinguishing between addresses actively assigned to pods and addresses available for future scheduling
  • Generating a detailed breakdown of IP consumption per node and ENI

This visibility enables you to identify capacity constraints before they impact workload deployment and make informed decisions about subnet expansion or architectural adjustments.

Pre-requisites

  • Python 3.12 or above installed locally. The latest boto3 package is recommended
  • Make sure the IAM role/IAM user configured locally has an entry on the EKS access policy that includes enough permissions to read pods and nodes.
  • The minimal permissions for the API calls to the AWS services are the following:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:DescribeSubnets"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": "eks:DescribeCluster",
      "Resource": "*"
    }
  ]
}

The script

import boto3
import ipaddress
import subprocess
import json
import sys

def get_subnet_cidr(ec2, subnet_id):
    return ec2.describe_subnets(SubnetIds=[subnet_id])['Subnets'][0]['CidrBlock']

def main():
    if len(sys.argv) != 3:
        print("Usage: check_eni_capacity.py <cluster-name> <region>")
        sys.exit(1)

    cluster_name = sys.argv[1]
    region = sys.argv[2]

    ec2 = boto3.client('ec2', region_name=region)

    # Update kubeconfig
    subprocess.run(['aws', 'eks', 'update-kubeconfig', '--name', cluster_name, '--region', region], check=True)

    # Get nodes from Kubernetes
    result = subprocess.run(['kubectl', 'get', 'nodes', '-o', 'json'], capture_output=True, text=True, check=True)
    nodes = json.loads(result.stdout)

    # Get pods from Kubernetes
    result = subprocess.run(['kubectl', 'get', 'pods', '-A', '-o', 'json'], capture_output=True, text=True, check=True)
    pods = json.loads(result.stdout)

    # Map pod IPs to nodes
    pod_ips_by_node = {}
    for pod in pods['items']:
        node_name = pod['spec'].get('nodeName')
        pod_ip = pod['status'].get('podIP')
        if node_name and pod_ip:
            pod_ips_by_node.setdefault(node_name, []).append(pod_ip)

    # Extract instance IDs from nodes
    instance_ids = []
    node_to_instance = {}
    for node in nodes['items']:
        node_name = node['metadata']['name']
        provider_id = node['spec'].get('providerID', '')
        if provider_id.startswith('aws:///'):
            instance_id = provider_id.split('/')[-1]
            instance_ids.append(instance_id)
            node_to_instance[instance_id] = node_name

    if not instance_ids:
        print("No nodes found in cluster")
        return

    # Get instance details
    instances = ec2.describe_instances(InstanceIds=instance_ids)

    for reservation in instances['Reservations']:
        for instance in reservation['Instances']:
            instance_id = instance['InstanceId']
            node_name = node_to_instance.get(instance_id, 'unknown')
            pod_ips = pod_ips_by_node.get(node_name, [])

            print(f"\n{'='*80}")
            print(f"Instance: {instance_id} (Node: {node_name})")
            print(f"{'='*80}")

            for eni in instance['NetworkInterfaces']:
                eni_id = eni['NetworkInterfaceId']
                primary_ip = eni['PrivateIpAddress']
                subnet_id = eni['SubnetId']
                subnet_cidr = get_subnet_cidr(ec2, subnet_id)

                print(f"\nENI: {eni_id}")
                print(f"  Primary IP: {primary_ip}")
                print(f"  Subnet: {subnet_id} ({subnet_cidr})")

                # Check for IPv4 prefixes
                if 'Ipv4Prefixes' in eni and eni['Ipv4Prefixes']:
                    total_ips_all = 0
                    assigned_all = 0
                    all_pod_ips = []

                    for prefix_info in eni['Ipv4Prefixes']:
                        prefix = prefix_info['Ipv4Prefix']
                        prefix_net = ipaddress.ip_network(prefix)
                        total_ips = prefix_net.num_addresses

                        # Count pod IPs in this prefix
                        pod_ips_in_prefix = sorted([ip for ip in pod_ips if ipaddress.ip_address(ip) in prefix_net],
                                                   key=lambda ip: ipaddress.ip_address(ip))
                        assigned = len(pod_ips_in_prefix)

                        total_ips_all += total_ips
                        assigned_all += assigned
                        all_pod_ips.extend(pod_ips_in_prefix)

                        available = total_ips - assigned

                        print(f"  Prefix: {prefix}")
                        print(f"    Total IPs in prefix: {total_ips}")
                        print(f"    Assigned IPs (pods): {assigned}")
                        if pod_ips_in_prefix:
                            print(f"    Pod IPs: {', '.join(pod_ips_in_prefix)}")
                        print(f"    Available for pods: {available}")

                    # Show aggregated totals if multiple prefixes
                    if len(eni['Ipv4Prefixes']) > 1:
                        available_all = total_ips_all - assigned_all
                        print(f"  --- ENI Totals ({len(eni['Ipv4Prefixes'])} prefixes) ---")
                        print(f"    Total IPs: {total_ips_all}")
                        print(f"    Assigned IPs (pods): {assigned_all}")
                        if all_pod_ips:
                            print(f"    All Pod IPs: {', '.join(all_pod_ips)}")
                        print(f"    Available for pods: {available_all}")
                else:
                    print(f"  No IPv4 prefixes assigned")

if __name__ == "__main__":
    main()

Sample outputs

For the default EKS Auto Mode, the following sample output can be expected:

================================================================================
Instance: i-aaaaaaaaaaaaaaaaa (Node: i-aaaaaaaaaaaaaaaaa)
================================================================================

ENI: eni-aaaaaaaaaaaaaaaaa
  Primary IP: 192.168.183.45
  Subnet: subnet-aaaaaaaaaaaaaaaaa (192.168.160.0/19)
  Prefix: 192.168.181.240/28
    Total IPs in prefix: 16
    Assigned IPs (pods): 1
    Pod IPs: 192.168.181.240
    Available for pods: 15

================================================================================
Instance: i-bbbbbbbbbbbbbbbbb (Node: i-bbbbbbbbbbbbbbbbb)
================================================================================

ENI: eni-bbbbbbbbbbbbbbbbb
  Primary IP: 192.168.81.38
  Subnet: subnet-bbbbbbbbbbbbbbbbb (192.168.64.0/19)
  Prefix: 192.168.71.80/28
    Total IPs in prefix: 16
    Assigned IPs (pods): 3
    Pod IPs: 192.168.71.80, 192.168.71.81, 192.168.71.82
    Available for pods: 13

================================================================================
Instance: i-ccccccccccccccccc (Node: i-ccccccccccccccccc)
================================================================================

ENI: eni-ccccccccccccccccc
  Primary IP: 192.168.71.108
  Subnet: subnet-bbbbbbbbbbbbbbbbb (192.168.64.0/19)
  Prefix: 192.168.85.160/28
    Total IPs in prefix: 16
    Assigned IPs (pods): 1
    Pod IPs: 192.168.85.161
    Available for pods: 15

Now, for the scenario where there is a separation of subnets (one for the main ENI of the worker node and another for the pods), the output looks similar to this:

================================================================================
Instance: i-aaaaaaaaaaaaaaaaa (Node: i-aaaaaaaaaaaaaaaaa)
================================================================================

ENI: eni-bbbbbbbbbbbbbbbbb
  Primary IP: 100.64.51.12
  Subnet: subnet-bbbbbbbbbbbbbbbbb (100.64.32.0/19)
  Prefix: 100.64.43.16/28
    Total IPs in prefix: 16
    Assigned IPs (pods): 3
    Pod IPs: 100.64.43.16, 100.64.43.17, 100.64.43.18
    Available for pods: 13

ENI: eni-aaaaaaaaaaaaaaaaa
  Primary IP: 192.168.137.198
  Subnet: subnet-aaaaaaaaaaaaaaaaa (192.168.128.0/19)
  No IPv4 prefixes assigned
  
================================================================================
Instance: i-0e8217b21d04ed516 (Node: i-0e8217b21d04ed516)
================================================================================

ENI: eni-ccccccccccccccccc
  Primary IP: 192.168.177.125
  Subnet: subnet-0e614ee078068825e (192.168.160.0/19)
  No IPv4 prefixes assigned

ENI: eni-ddddddddddddddddd
  Primary IP: 100.64.70.70
  Subnet: subnet-bbbbbbbbbbbbbbbbb (100.64.64.0/19)
  Prefix: 100.64.72.176/28
    Total IPs in prefix: 16
    Assigned IPs (pods): 2
    Pod IPs: 100.64.72.176, 100.64.72.177
    Available for pods: 14

References: