cloudNet@ 팀의 가시다 님이 진행하는 AWS EKS Workshop Study(AEWS) 4기 2주차 내용입니다.
실습 환경 배포
# 코드 다운로드, 작업 디렉터리 이동
git clone https://github.com/gasida/aews.git
cd aews/2w
# 변수 지정
export TF_VAR_KeyName=$(aws ec2 describe-key-pairs --query "KeyPairs[].KeyName" --output text)
export TF_VAR_ssh_access_cidr=$(curl -s ipinfo.io/ip)/32
echo $TF_VAR_KeyName $TF_VAR_ssh_access_cidr
# 배포 : 12분 정도 소요
terraform init
terraform plan
nohup sh -c "terraform apply -auto-approve" > create.log 2>&1 &
tail -f create.log
# 자격증명 설정
terraform output -raw configure_kubectl
aws eks --region ap-northeast-2 update-kubeconfig --name myeks
aws eks --region ap-northeast-2 update-kubeconfig --name myeks
kubectl config rename-context $(cat ~/.kube/config | grep current-context | awk '{print $2}') myeks
배포 후 기본 정보 확인
- EKS 관리 콘솔 확인
- Overview 개요 : API server endpoint, Open ID Connect provider URL기본 정보(oidc)
- Compute 컴퓨팅 : Node groups 클릭 → 상세 정보 확인 ⇒ kubernetes 레이블 tier = primary
- Networking 네트워킹 : 서비스 IPv4 범위(10.100.0.0/16), 서브넷, access(public and private)..
- Add-ons 추가 기능 : VPC CNI 클릭 후 추가 정보 확인

EKS 기본 정보 확인
# 클러스터 확인
kubectl cluster-info
eksctl get cluster
# 네임스페이스 default 변경 적용
kubens default
# 노드 정보 확인
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
kubectl get node -v=6
# 노드 라벨 확인
kubectl get node --show-labels
kubectl get node -l tier=primary
# 파드 정보 확인
kubectl get pod -A
kubectl get pdb -n kube-system
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
coredns N/A 1 1 28m
metrics-server N/A 1 1 28m
# 관리형 노드 그룹 확인
aws eks describe-nodegroup --cluster-name myeks --nodegroup-name myeks-1nd-node-group | jq
# eks addon 확인
aws eks list-addons --cluster-name myeks | jq
eksctl get addon --cluster myeks
NAME VERSION STATUS ISSUES IAMROLE UPDATE AVAILABLE CONFIGURATION VALUES NAMESPACE POD IDENTITY ASSOCIATION ROLES
coredns v1.13.2-eksbuild.3 ACTIVE 0 kube-system
kube-proxy v1.34.5-eksbuild.2 ACTIVE 0 kube-system
vpc-cni v1.21.1-eksbuild.5 ACTIVE 0 {"env":{"WARM_ENI_TARGET":"1"}} kube-system
- EC2 관리 콘솔 확인 : type, az, IP, ec2 instance profile → iam role 확인

AWS VPC CNI란
1. Kubernetes 네트워킹 모델과 CNI
K8s CNI는 Kubernetes 네트워크 환경을 구성하는 핵심 표준으로 고유의 네트워킹 모델을 구현하기 위해다양한 플러그인을 지원한다. - 링크 ( Calico , Cilium )
Kubernetes 네트워킹 모델의 주요 요구사항
Kubernetes 환경이 정상적으로 동작하기 위해서는 다음과 같은 네트워크 통신 조건이 반드시 충족되어야 한다.
- 동일한 노드에 배치된 Pod는 NAT를 거치지 않고 다른 포드와 직접 통신할 수 있어야 한다.
- 특정 노드에서 실행되는 모든 시스템 데몬(kubelet 등 백그라운드 프로세스)은 동일한 노드에서 실행되는 Pod와 원활하게 통신할 수 있어야 한다.
- 호스트 네트워크(Host Network)를 사용하는 Pod는 NAT 없이 다른 모든 노드의 다른 모든 포드에 연결할 수 있어야 한다.
- 파드 설정 시 hostNetwork: true 옵션을 활성화하면, 해당 파드는 자신만의 격리된 네트워크 네임스페이스를 배정받지 않는다. 대신 워커 노드의 전역 네트워크 네임스페이스에 직접 위치하게 되며, 노드(EC2)의 실제 IP 주소와 네트워크 인터페이스(eth0)를 자신의 것처럼 그대로 공유한다.
이러한 구조 속에서 파트 네트워크 네임스페이스와 호스트 네트워크 네임스페이스는 브릿지(Bridge)나 veth 페어 등을 통해 상호 연결되는 관계를 맺게 된다.

- 네트워크 네임스페이스: 논리적으로 완전히 분리된 '독립적인 네트워크 방'이다.
- Host Network Namespace: 노드(EC2) 전체를 아우르는 '거실'이다. 실제 물리적인 네트워크 카드(eth0)가 여기에 존재한다.
- Pod Network Namespace: 파드가 생성될 때마다 만들어지는 '개별 방'이다. 하나의 방(Pod) 안에는 여러 컨테이너(Container 1, 2)가 함께 살며, 이들은 localhost를 서로 공유한다.
- veth 페어 (Virtual Ethernet Pair): 이렇게 논리적으로 격리된 두 네트워크 네임스페이스(Host와 Pod)를 서로 연결해 주는 가상의 L2 링크이다. 마치 물리적인 랜 케이블의 양 끝단처럼 항상 쌍(Pair)으로 생성되며, 한쪽 인터페이스는 파드 내부(eth0)에, 다른 한쪽 인터페이스는 호스트 네임스페이스(veth0, veth1 등)에 연결되어 두 격리 공간 사이의 브릿지 역할을 수행한다.
2. 워커 노드에서의 CNI 구성 및 동작 방식
CNI 플러그인은 Kubelet 프로세스에 --network-plugin=cni 명령줄 옵션을 전달함으로써 활성화된다. Kubelet은 다음과 같은 디렉터리를 참조하여 네트워크를 설정한다.
- 설정 파일 경로 (--cni-conf-dir): 기본값은 /etc/cni/net.d 이며, 이 경로에서 CNI 구성 파일을 읽어 들여 각 포드의 네트워크를 설정한다. 구성 파일이 여러 개일 경우, 사전적(어휘) 순서로 가장 먼저 오는 파일을 채택한다. (CNI 사양 최소 v0.4.0 일치 필요)
- 바이너리 경로 (--cni-bin-dir): 기본값은 /opt/cni/bin 이며, 구성 파일에서 참조하는 실제 CNI 플러그인 실행 파일들이 이곳에 위치해야 한다.
cat /etc/cni/net.d/10-aws.conflist | jq

3. Amazon VPC CNI의 핵심 아키텍처와 동작 원리
Amazon VPC CNI는 AWS EKS 클러스터를 프로비저닝할 때 기본적으로 설치되는 네트워킹 애드온이다. 이 플러그인의 가장 큰 특징은 별도의 오버레이(Overlay) 네트워크를 구성하지 않고, 파드에 AWS VPC의 실제 사설 IP 주소를 직접 할당한다는 점이다.


- 파드 내의 모든 컨테이너는 네트워크 네임스페이스를 공유하며 로컬 포트를 사용하여 서로 통신할 수 있다.
- 일반적으로 K8S CNI는 오버레이(VXLAN, IP-IP 등) 통신을 하고, AWS VPC CNI는 동일 대역으로 직접 통신한다.
즉 파드의 네트워크 대역과 워커 노드의 네트워크 대역이 동일해지며 파드는 VPC 내의 다른 AWS 리소스와 NAT 과정 없이 직접 통신할 수 있다. 또한, VPC Flow Logs, 라우팅 정책, 보안 그룹과 같은 AWS 네이티브 네트워크 기능을 파드 레벨에서 그대로 활용할 수 있다는 장점을 제공한다.
Amazon VPC CNI는 Kubernetes 워커 노드 내부에서 aws-node라는 이름의 데몬셋 파드 형태로 실행되며 내부적으로 크게 두 가지 핵심 컴포넌트로 역할을 분담하여 동작한다.
1. CNI 바이너리 (/opt/cni/bin/aws-cni) : 네트워크 인터페이스 구성의 실행자
- 노드의 루트 파일 시스템에 위치하며, 실제 컨테이너의 네트워크 인터페이스를 구성하는 실무를 전담한다.
- Kubelet은 새로운 파드가 노드에 할당(배포)되거나 기존 파드가 삭제될 때마다 이 CNI 바이너리를 호출한다.
- 호출된 바이너리는 노드의 호스트 네트워크 네임스페이스와 파드의 격리된 네트워크 네임스페이스 사이에 가상의 랜선인 veth 페어를 구성하고, 할당된 IP를 파드에 매핑하는 작업을 수행한다.
2. ipamd (IP Address Management Daemon) : ENI 관리 및 웜 풀(Warm-pool) 유지
- 워커 노드에서 백그라운드로 실행되는 L-IPAM(Local-IPAM) 컴포넌트로, AWS EC2 컨트롤 플레인(API)과 지속적으로 통신하며 노드에 ENI를 연결하고 수명 주기를 관리한다.
- 웜 풀(Warm-pool)을 통한 IP 할당: 파드가 생성될 때마다 노드가 중앙의 AWS API를 호출하여 새로운 IP를 요청한다면 필연적으로 통신 지연(Latency)이 발생한다. 이를 방지하기 위해 ipamd는 미리 일정량의 보조 IP(Secondary IP)나 접두사(Prefix)를 ENI에 할당받아, 언제든 즉시 꺼내 쓸 수 있는 '웜 풀(Warm-pool)' 형태로 노드 로컬에 대기시켜 둔다.
- 결과적으로 Kubelet이 파드 생성을 요청하면, 무거운 API 호출 과정 없이 웜 풀에 미리 준비되어 있던 IP가 즉시 제공되므로 파드가 매우 빠른 속도로 구동(Fast Pod Startup)될 수 있다.

4. 파드 IP 할당 과정
4.1. 파드가 생성될 때 노드 내부에서는 아래와 같은 3단계 릴레이가 일어난다.
- Kubelet의 명령 하달 (CNI add/del): 워커 노드의 Kubelet이 새로운 파드를 스케줄링받으면, 일회성 실행 파일인 CNI Plugin을 깨워서 "새 파드가 떴으니 네트워크를 연결해!"라고 명령을 내린다.
- gRPC를 통한 IP 요청: 명령을 받은 CNI Plugin은 실제 IP를 쥐고 있지 않다. 따라서 노드에 24시간 상주하며 IP Warm Pool을 관리하는 L-IPAM에게 파드에 꽂아줄 IP 하나만 내어줘!라고 요청한다. 이때 둘 사이를 이어주는 통신 규격이 바로 gRPC다.
- gRPC를 통한 IP 즉시 응답: 요청을 받은 L-IPAM은 외부(AWS 컨트롤 플레인)로 나가지 않고, 자신이 미리 확보해 둔 Warm Pool에서 사용 가능한 IP 하나를 즉시 꺼내어 CNI Plugin에게 응답으로 건네준다.

4.2. gRPC를 사용하는 이유
단순한 함수 호출이 아니라 굳이 네트워크 통신 프로토콜인 gRPC가 등장하는 데는 명확한 아키텍처적 이유가 있다.
- 수명이 다른 두 프로세스 간의 통신 (IPC): CNI Plugin 바이너리는 파드가 뜰 때 잠시 실행되었다가 할 일을 마치면 바로 종료되는 단기 프로세스다. 반면 L-IPAM은 노드가 켜져 있는 내내 백그라운드에서 돌아가는 장기 실행 데몬이다. 이렇게 서로 분리된 두 프로그램이 데이터를 주고받으려면 프로세스 간 통신이 필수적인데, 쿠버네티스 생태계에서는 이 통신의 표준으로 빠르고 안정적인 gRPC를 채택하고 있다.
- 초고속, 고효율 대용량 처리: 마이크로서비스 환경에서는 수십~수백 개의 파드가 동시에 뜨고 지는 일이 빈번하다. gRPC는 HTTP/2를 기반으로 동작하여 매우 가볍고 빠르며, 데이터를 압축해서 주고받기 때문에 IP 할당 요청이 폭주하더라도 병목 현상 없이 순식간에 처리해 낼 수 있다.
- 네트워크 외부가 아닌 노드 내부 통신: 이 다이어그램에서 가장 중요한 포인트다. 이 gRPC 통신은 AWS의 무거운 API 서버로 날아가는 원격 요청이 아니다. 워커 노드 1대 안에서, 두 프로세스가 자기들끼리만 주고받는 내부 통신이다. 밖으로 나가는 Latency 없이 노드 안에서 gRPC로 즉각적인 처리가 이루어지기 때문에, EKS의 파드가 찰나의 순간에 초고속으로 구동될 수 있는 것이다.
5. 확장성과 보안을 위한 VPC CNI 주요 구성 모드
클러스터의 규모, 파드 밀도, 보안 요구사항에 따라 Amazon VPC CNI는 다양한 구성 옵션을 제공한다. 클러스터 설계 시 각 모드의 특징을 명확히 이해하고 적용하는 것이 중요하다.
5.1. 보조 IP 모드 (Secondary IP Mode) - 기본값
Amazon VPC CNI의 기본 동작 방식이다. 노드에 부착된 기본 ENI 및 추가 ENI들에 **보조 프라이빗 IPv4 주소(Secondary Private IP)**를 뭉텅이로 할당받아 파드에 하나씩 1:1로 매핑한다.
- 한계점: EC2 인스턴스 유형(Type)마다 부착할 수 있는 최대 ENI 개수와, ENI당 할당 가능한 보조 IP 개수가 하드 리밋으로 정해져 있다. 즉, 인스턴스 크기가 작으면 노드 하나당 띄울 수 있는 파드의 최대 개수(파드 밀도)가 크게 제한된다.

5.2. 접두사 위임 모드 (Prefix Delegation Mode)
앞서 언급한 보조 IP 모드의 '파드 밀도 제한' 문제를 해결하기 위해 도입된 기능이다. 개별 IP 주소 하나씩을 할당받는 대신, **/28 크기의 IPv4 CIDR 접두사(Prefix, IP 16개 묶음)**를 ENI의 슬롯에 통째로 할당한다.
- 효과: 이 모드를 활성화하면 상대적으로 크기가 작은 EC2 인스턴스(예: m5.large)에서도 기존보다 훨씬 더 많은 수의 파드를 구동할 수 있어 리소스 효율성이 극대화된다. (대규모 마이크로서비스 환경에서 필수적으로 고려됨)

5.3. 웜 풀 최적화 및 IP 낭비 방지 전략
Secondary IP Mode를 사용할 때 가장 큰 딜레마는 파드의 빠른 시작 속도와 VPC IP 자원 절약 사이의 트레이드오프다.
ipamd는 파드가 즉시 사용할 수 있도록 빈 IP(웜 풀)를 노드에 미리 확보해 둔다. 하지만 무작정 많은 ENI와 IP를 미리 끌어다 놓으면 정작 다른 AWS 리소스가 사용할 IP가 고갈되는 현상이 발생한다. 이를 세밀하게 제어하기 위해 VPC CNI는 aws-node 데몬셋의 환경 변수를 통한 튜닝 옵션을 제공한다.
1. WARM_ENI_TARGET (ENI 단위 확보)
- 정의: 노드에 항상 여유분으로 남겨둘 빈 ENI의 개수다. (기본값: 1)
- 특징: 값이 1이라면, 파드가 추가될 것을 대비해 냅다 빈 ENI를 통째로 하나 더 노드에 부착해 버린다. 파드 스케일링 대응 속도는 가장 빠르지만, ENI에 딸려오는 수많은 보조 IP들이 사용되지 않은 채 방치되므로 IP 자원 낭비가 매우 심하다. IP가 부족한 환경에서는 값을 0으로 설정하여 비활성화하는 것이 일반적이다.
2. WARM_IP_TARGET (IP 단위 정밀 제어)
- 정의: 노드에 항상 여유분으로 유지할 '빈 IP 주소'의 개수다.
- 특징: 이 값이 10이라면, 파드가 몇 개가 떠 있든 상관없이 항상 즉시 할당 가능한 IP 10개를 확보해 둔다. ENI 단위가 아닌 IP 단위로 웜 풀을 관리하기 때문에 자원 낭비를 최소화하면서도 파드의 생성 속도를 보장할 수 있는 권장 옵션이다.
3. MINIMUM_IP_TARGET (초기 최소 확보량)
- 정의: 워커 노드가 처음 구동될 때 기본적으로 확보해야 하는 최소한의 IP 총 개수다.
- 특징: 보통 WARM_IP_TARGET과 조합하여 사용한다. 클러스터 생성 직후 대규모 트래픽이 유입되어 파드가 급격히 늘어나는(Scale-out) 상황에서, 초기 IP 할당 지연을 막기 위한 든든한 초기 자본금 역할을 한다.
4. aws-node 데몬셋에서 관련 env 확인
# aws-node DaemonSet의 env 확인
kubectl get ds aws-node -n kube-system -o json | jq '.spec.template.spec.containers[0].env'
kubectl describe ds aws-node -n kube-system | grep -E "WARM_ENI_TARGET|WARM_IP_TARGET|MINIMUM_IP_TARGET"
kubectl get daemonset aws-node --show-managed-fields -n kube-system -o yaml

5.4. 파드 보안 그룹 (Security Groups for Pods)
기본적으로 파드는 자신이 떠 있는 워커 노드(EC2)의 기본 ENI에 연결된 보안 그룹 규칙을 그대로 따라간다. 하지만 보안이 중요한 환경에서는 파드마다 접근 제어를 다르게 가져가야 한다.
- 이 기능을 활성화하면, 노드 레벨이 아닌 개별 파드 레벨에 별도의 독립적인 AWS 보안 그룹을 직접 연결할 수 있다. 트래픽 격리가 엄격하게 요구되는 금융권이나 엔터프라이즈 환경의 규정 준수(Compliance)를 충족하는 데 핵심적인 역할을 한다.

5.5. 사용자 지정 네트워킹 (Custom Networking)
VPC CNI가 파드에 실제 VPC IP를 할당하다 보니, 수백~수천 개의 파드가 뜨고 지는 대규모 클러스터에서는 VPC의 가용 IP(RFC1918)가 빠르게 고갈되는 문제가 발생한다.
- 해결책: 사용자 지정 네트워킹을 사용하면 노드가 통신하는 기본 대역과 완전히 분리된 별도의 '보조 CIDR'을 파드 전용으로 할당할 수 있다. 특히 외부와 라우팅되지 않는 100.64. 0.0/10 (CG-NAT) 대역을 활용하여 파드 IP 범위를 구성하는 것이 IP 고갈을 막는 가장 강력한 모범 사례(Best Practice)로 꼽힌다.

5.6. 단계별 파드 네트워크 구성
파드 통신을 위한 IPtables, 라우팅 설정 과정 : kubelet 에서 CNI 바이너리에 CNI 추가/삭제 요청을 통해 처리. 이때 L-IPAM 조회 확인

파드 네트워크 환경 구성(파드 네트워크 네임스페이스) : kubelet → vpc cni → L-IPAM → kernel API ⇒ kubelet → New Pod

- Kubelet이 포드 추가 요청을 수신하면 CNI 바이너리가 사용 가능한 IP 주소에 대해 ipamd를 쿼리한 다음 ipamd가 포드에 제공합니다. CNI 바이너리가 호스트 및 포드 네트워크를 연결합니다.
- 노드에 배포된 포드는 기본적으로 기본 ENI와 동일한 보안 그룹에 할당됩니다. 또는 다른 보안 그룹으로 포드를 구성할 수 있습니다.
- IP 주소 풀이 고갈되면 플러그 인은 다른 탄력적 네트워크 인터페이스를 인스턴스에 자동으로 연결하고 이 인터페이스에 다른 보조 IP 주소 집합을 할당합니다. 이 프로세스는 노드가 탄력적 네트워크 인터페이스를 추가 지원할 수 없을 때까지 계속됩니다.

- 포드가 삭제되면 VPC CNI는 포드의 IP 주소를 30초 cool down cache 에 배치합니다.
- cool down cache IPs는 새 포드에 할당되지 않습니다.
- cooling-off 쿨링 오프 기간이 끝나면 VPC CNI는 포드 IP를 웜 풀로 다시 이동합니다.
- 쿨링 오프 기간은 포드 IP 주소가 조기에 재활용되는 것을 방지하고 모든 클러스터 노드에서 kube-proxy가 iptables 규칙 업데이트를 완료하도록 허용합니다.
- IPs 또는 ENIs 수가 웜 풀 설정 수를 초과하면 ipamd 플러그인은 IPs 및 ENIs에 반환합니다.
노드에서 기본 네트워크 정보 확인
1. 노드 접속 및 IP 변수 지정
# EC2 ENI IP 확인
aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
# 아래 IP는 각자 실습 환경에 따라 사용
N1=43.203.234.139
N2=54.116.45.208
N3=52.78.192.202
# 워커 노드 SSH 접속
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh -o StrictHostKeyChecking=no ec2-user@$i hostname; echo; done
2. 네트워크 기본 정보 확인
# 파드 상세 정보 확인
kubectl get daemonset aws-node --namespace kube-system -owide
kubectl describe daemonset aws-node --namespace kube-system
# kube-proxy config 확인 : 모드 iptables 사용
kubectl describe cm -n kube-system kube-proxy-config
...
mode: "iptables"
...
kubectl describe cm -n kube-system kube-proxy-config | grep iptables: -A5
iptables:
masqueradeAll: false
masqueradeBit: 14
minSyncPeriod: 0s
syncPeriod: 30s
3. 노드에 네트워크 정보 확인
# cni log 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i tree /var/log/aws-routed-eni ; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo cat /var/log/aws-routed-eni/plugin.log | jq ; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo cat /var/log/aws-routed-eni/ipamd.log | jq ; echo; done
# 네트워크 정보 확인 : eniY는 pod network 네임스페이스와 veth pair
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -br -c addr; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c addr; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done
ssh ec2-user@$N1 sudo iptables -t nat -S
ssh ec2-user@$N1 sudo iptables -t nat -L -n -v

4. 워커 노드1 기본 네트워크 구성

- Network 네임스페이스는 호스트(Root)와 파드 별(Per Pod)로 구분된다
- 특정한 파드(kube-proxy, aws-node)는 호스트(Root)의 IP를 그대로 사용한다 ⇒ 파드의 Host Network 옵션
- t3.medium 의 경우 ENI 마다 최대 6개의 IP를 가질 수 있다
- ENI0, ENI1 으로 2개의 ENI는 자신의 IP 이외에 추가적으로 5개의 보조 프라이빗 IP를 가질수 있다
- coredns 파드는 veth 으로 호스트에는 eniY@ifN 인터페이스와 파드에 eth0 과 연결되어 있다
워커 노드1 인스턴스의 네트워크 정보 확인 : 프라이빗 IP와 보조 프라이빗 IP 확인


5. 보조 IPv4 주소를 coredns 파드가 사용하는지 확인 ⇒ coredns 파드가 배치되지 않은 워커 노드에 ENI 갯수 확인!
# coredns 파드 IP 정보 확인
kubectl get pod -n kube-system -l k8s-app=kube-dns -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
coredns-d487b6fcb-7x24g 1/1 Running 0 13m 192.168.1.63 ip-192-168-1-36.ap-northeast-2.compute.internal <none> <none>
coredns-d487b6fcb-vb248 1/1 Running 0 13m 192.168.3.133 ip-192-168-3-208.ap-northeast-2.compute.internal <none> <none>
# 노드의 라우팅 정보 확인 >> EC2 네트워크 정보의 '보조 프라이빗 IPv4 주소'와 비교해보자
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done
# IpamD debugging commands
# https://github.com/aws/amazon-vpc-cni-k8s/blob/master/docs/troubleshooting.md
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s http://localhost:61679/v1/enis | jq; echo; done
6. Network-Multitool 디플로이먼트 생성 - https://github.com/Praqma/Network-MultiTool
# [터미널1~3] 노드 모니터링
ssh ec2-user@$N1
watch -d "ip link | egrep 'ens|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
ssh ec2-user@$N2
watch -d "ip link | egrep 'ens|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
ssh ec2-user@$N3
watch -d "ip link | egrep 'ens|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
# Network-Multitool 디플로이먼트 생성
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: netshoot-pod
spec:
replicas: 3
selector:
matchLabels:
app: netshoot-pod
template:
metadata:
labels:
app: netshoot-pod
spec:
containers:
- name: netshoot-pod
image: praqma/network-multitool
ports:
- containerPort: 80
- containerPort: 443
env:
- name: HTTP_PORT
value: "80"
- name: HTTPS_PORT
value: "443"
terminationGracePeriodSeconds: 0
EOF
# 파드 이름 변수 지정
PODNAME1=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[0].metadata.name}')
PODNAME2=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[1].metadata.name}')
PODNAME3=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[2].metadata.name}')
echo $PODNAME1 $PODNAME2 $PODNAME3
# 파드 확인
kubectl get pod -o wide
kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP
# 노드에 라우팅 정보 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done
- 파드가 생성되면, 워커 노드에 eniY@ifN 추가되고 라우팅 테이블에도 정보가 추가된다
- 테스트용 파드 eniY 정보 확인 - 워커 노드 EC2
# 노드3에서 네트워크 인터페이스 정보 확인
ssh ec2-user@$N3
----------------
ip -br -c addr show
ip -c link
ip -c addr
ip route # 혹은 route -n
# 네임스페이스 정보 출력 -t net(네트워크 타입)
sudo lsns -t net
exit
----------------
- 테스트용 파드 접속(exec) 후 확인
# 테스트용 파드 접속(exec) 후 Shell 실행
kubectl exec -it $PODNAME1 -- bash
# 아래부터는 pod-1 Shell 에서 실행 : 네트워크 정보 확인
----------------------------
ip -c addr
ip -c route
route -n
ping -c 1 <pod-2 IP>
ps
cat /etc/resolv.conf
exit
----------------------------
# 파드2 Shell 실행
kubectl exec -it $PODNAME2 -- ip -c addr
# 파드3 Shell 실행
kubectl exec -it $PODNAME3 -- ip -br -c addr
노드 간 파드 통신
- 목표 : 파드간 통신 시 tcpdump 내용을 확인하고 통신 과정을 알아본다
- 파드간 통신 흐름 : AWS VPC CNI 경우 별도의 오버레이(Overlay) 통신 기술 없이, VPC Native 하게 파드간 직접 통신이 가능하다

- 파드간 통신 시 과정 참고

- [실습] 파드간 통신 테스트 및 확인 : 별도의 NAT 동작 없이 통신 가능!
# 파드 IP 변수 지정
PODIP1=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[0].status.podIP}')
PODIP2=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[1].status.podIP}')
PODIP3=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[2].status.podIP}')
echo $PODIP1 $PODIP2 $PODIP3
# 파드1 Shell 에서 파드2로 ping 테스트
kubectl exec -it $PODNAME1 -- ping -c 2 $PODIP2
kubectl exec -it $PODNAME1 -- curl -s http://$PODIP2
kubectl exec -it $PODNAME1 -- curl -sk https://$PODIP2
# 파드2 Shell 에서 파드3로 ping 테스트
kubectl exec -it $PODNAME2 -- ping -c 2 $PODIP3
# 파드3 Shell 에서 파드1로 ping 테스트
kubectl exec -it $PODNAME3 -- ping -c 2 $PODIP1
# 워커 노드 EC2 : TCPDUMP 확인
## For Pod to external (outside VPC) traffic, we will program iptables to SNAT using Primary IP address on the Primary ENI.
sudo tcpdump -i any -nn icmp
sudo tcpdump -i ens5 -nn icmp
sudo tcpdump -i ens6 -nn icmp
sudo tcpdump -i eniYYYYYYYY -nn icmp
[워커 노드1]
# routing policy database management 확인
ip rule
# routing table management 확인
ip route show table local
ip route show table main
ip route show table 2
(심화) Proposal: CNI plugin for Kubernetes networking over AWS VPC - Docs
- Inside a Pod
# IP address
ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
3: eth0@if231: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 56:41:95:26:17:41 brd ff:ff:ff:ff:ff:ff
inet 10.0.97.30/32 brd 10.0.97.226 scope global eth0 <<<<<<< ENI's secondary IP address
valid_lft forever preferred_lft forever
inet6 fe80::5441:95ff:fe26:1741/64 scope link
valid_lft forever preferred_lft forever
# routes
ip route show
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0
# static arp
arp -a
? (169.254.1.1) at 2a:09:74:cd:c4:62 [ether] PERM on eth0
- On Host side : There are multiple routing tables used to route incoming/outgoing Pod's traffic.
# main (toPod) route table is used to route to Pod traffic
ip route show
default via 10.0.96.1 dev eth0
10.0.96.0/19 dev eth0 proto kernel scope link src 10.0.104.183
10.0.97.30 dev aws8db0408c9a8 scope link <------------------------ # Pod's IP
10.0.97.159 dev awsbcd978401eb scope link
10.0.97.226 dev awsc2f87dc4cdd scope link
10.0.102.98 dev aws4914061689b scope link
...
# Each ENI has its own route table which is used to route pod's outgoing traffic, where pod is allocated with one of the ENI's secondary IP address
ip route show table eni-1
default via 10.0.96.1 dev eth1
10.0.96.1 dev eth1 scope link
# Here is the routing rules to enforce policy routing
ip rule list
0: from all lookup local
512: from all to 10.0.97.30 lookup main <---------- # to Pod's traffic
1025: not from all to 10.0.0.0/16 lookup main
1536: from 10.0.97.30 lookup eni-1 <----------- # from Pod's traffic
#----------------------------------------------------
ip rule list
0: from all lookup local
512: from all to 192.168.3.36 lookup main
512: from all to 192.168.3.218 lookup main
512: from all to 192.168.3.171 lookup main
1024: from all fwmark 0x80/0x80 lookup main
1536: from 192.168.3.171 lookup 2
32765: from 192.168.3.233 lookup 2
32766: from all lookup main
32767: from all lookup default
ip route show table 2
default via 192.168.3.1 dev ens6
192.168.3.1 dev ens6 scope link
- CNI Plugin Sequence : Here are the wiring steps to enable pod to pod communication
# Get a Secondary IP address assigned to the instance by L-IPAMD
# Create a veth pair and have one veth on host namespace and one veth on Pod's namespace
ip link add veth-1 type veth peer name veth-1c /* on host namespace */
ip link set veth-1c netns ns1 /* move veth-1c to "Pod's" namespace ns1 */
ip link set veth-1 up /* bring up veth-1 */
ip netns exec ns1 ip link set veth-1c up /* bring up veth-1c */
# Perform following inside Pod's name space:
## Assign the IP address to Pod's eth0
## Add default gateway and default route to Pod's route table
## Add a static ARP entry for default gateway
/* To assign IP address 20.0.49.215 to "Pod's" namespace ns1 */
ip netns exec ns1 ip addr add 20.0.49.215/32 dev veth-1c /* assign a IP address to veth-1c */
ip netns exec ns1 ip route add 169.254.1.1 dev veth-1c /* add default gateway */
ip netns exec ns1 ip route add default via 169.254.1.1 dev veth-1c /* add default route */
ip netns exec ns1 arp -i veth-1c -s 169.254.1.1 <"veth-1's" mac> /* add static ARP entry for default gateway */
# On host side, add host route and routing rule so that incoming Pod's traffic can be routed to Pod.
/* "Pod's" IP address is 20.0.49.215 */
ip route add 20.0.49.215/32 dev veth-1 /* add host route */
ip rule add from all to 20.0.49.215/32 table main prio 512 /* add routing rule */
# Note:
## When Pod's ip address is associated with the secondary ENI, the from-pod rule is also added.
## For example, if the ip address "20.0.49.215" is associated with eth1, add the following rule.
ip rule add from 20.0.49.215/32 table 2 prio 1536 /* add from-pod rule */
파드에서 외부 통신
파드에서 외부 통신 흐름 : iptable 에 SNAT 을 통하여 노드의 eth0(ens5) IP로 변경되어서 외부와 통신됨

(참고) VPC CNI 의 External source network address translation (SNAT) 설정에 따라, 외부(인터넷) 통신 시 SNAT 하거나 혹은 SNAT 없이 통신을 할 수 있다 - 링크
- [실습] 파드에서 외부 통신 테스트 및 확인
- 파드 shell 실행 후 외부로 ping 테스트 & 워커 노드에서 tcpdump 및 iptables 정보 확인
# pod-1 Shell 에서 외부로 ping
kubectl exec -it $PODNAME1 -- ping -c 1 www.google.com
kubectl exec -it $PODNAME1 -- ping -i 0.1 www.google.com
kubectl exec -it $PODNAME1 -- ping -i 0.1 8.8.8.8
# 워커 노드 EC2 : TCPDUMP 확인
sudo tcpdump -i any -nn icmp
sudo tcpdump -i ens5 -nn icmp
# 퍼블릭IP 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s ipinfo.io/ip; echo; echo; done
# 작업용 EC2 : pod-1 Shell 에서 외부 접속 확인 - 공인IP는 어떤 주소인가?
## The right way to check the weather - 링크
for i in $PODNAME1 $PODNAME2 $PODNAME3; do echo ">> Pod : $i <<"; kubectl exec -it $i -- curl -s ipinfo.io/ip; echo; echo; done
kubectl exec -it $PODNAME1 -- curl -s wttr.in/seoul
kubectl exec -it $PODNAME1 -- curl -s wttr.in/seoul?format=3
kubectl exec -it $PODNAME1 -- curl -s wttr.in/Moon
kubectl exec -it $PODNAME1 -- curl -s wttr.in/:help
# 워커 노드 EC2
## 출력된 결과를 보고 어떻게 빠져나가는지 고민해보자!
ip rule
ip route show table main
sudo iptables -L -n -v -t nat
sudo iptables -t nat -S
# 파드가 외부와 통신시에는 아래 처럼 'AWS-SNAT-CHAIN-0' 룰(rule)에 의해서 SNAT 되어서 외부와 통신!
# 참고로 뒤 IP는 eth0(ENI 첫번째)의 IP 주소이다
# --random-fully 동작 - 링크1 링크2
sudo iptables -t nat -S | grep 'A AWS-SNAT-CHAIN'
-A AWS-SNAT-CHAIN-0 ! -d 192.168.0.0/16 -m comment --comment "AWS SNAT CHAIN" -j RETURN
-A AWS-SNAT-CHAIN-0 ! -o vlan+ -m comment --comment "AWS, SNAT" -m addrtype ! --dst-type LOCAL -j SNAT --to-source 192.168.1.251 --random-fully
## 아래 'mark 0x4000/0x4000' 매칭되지 않아서 RETURN 됨!
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
-A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully
...
# 카운트 확인 시 AWS-SNAT-CHAIN-0에 매칭되어, 목적지가 192.168.0.0/16 아니고 외부 빠져나갈때 SNAT 192.168.1.251(EC2 노드1 IP) 변경되어 나간다!
sudo iptables -t filter --zero; sudo iptables -t nat --zero; sudo iptables -t mangle --zero; sudo iptables -t raw --zero
watch -d 'sudo iptables -v --numeric --table nat --list AWS-SNAT-CHAIN-0; echo ; sudo iptables -v --numeric --table nat --list KUBE-POSTROUTING; echo ; sudo iptables -v --numeric --table nat --list POSTROUTING'
# conntrack 확인 : EC2 메타데이터 주소(169.254.169.254) 제외 출력
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo conntrack -L -n |grep -v '169.254.169'; echo; done
conntrack v1.4.5 (conntrack-tools):
icmp 1 28 src=172.30.66.58 dst=8.8.8.8 type=8 code=0 id=34392 src=8.8.8.8 dst=172.30.85.242 type=0 code=0 id=50705 mark=128 use=1
tcp 6 23 TIME_WAIT src=172.30.66.58 dst=34.117.59.81 sport=58144 dport=80 src=34.117.59.81 dst=172.30.85.242 sport=80 dport=44768 [ASSURED] mark=128 use=1
- 다음 실습을 위해서 디플로이먼트 삭제: kubectl delete deploy netshoot-pod
AWS VPC CNI 설정 변경
- 현재 상태 확인
# aws-node DaemonSet의 env 확인
kubectl get ds aws-node -n kube-system -o json | jq '.spec.template.spec.containers[0].env'
...
{
"name": "WARM_ENI_TARGET",
"value": "1"
},
...
# 노드 정보 확인 : 노드 중 1대는 eni 가 1개만 배치됨!
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c addr; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done
# IpamD debugging commands https://github.com/aws/amazon-vpc-cni-k8s/blob/master/docs/troubleshooting.md
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s http://localhost:61679/v1/enis | jq; echo; done
AWS VPC CNI 설정 변경 적용
- eks.tf 수정
# add-on
addons = {
coredns = {
most_recent = true
}
kube-proxy = {
most_recent = true
}
vpc-cni = {
most_recent = true
before_compute = true
configuration_values = jsonencode({
env = {
#WARM_ENI_TARGET = "1" # 현재 ENI 외에 여유 ENI 1개를 항상 확보
WARM_IP_TARGET = "5" # 현재 사용 중인 IP 외에 여유 IP 5개를 항상 유지, 설정 시 WARM_ENI_TARGET 무시됨
MINIMUM_IP_TARGET = "10" # 노드 시작 시 최소 확보해야 할 IP 총량 10개
#ENABLE_PREFIX_DELEGATION = "true"
#WARM_PREFIX_TARGET = "1" # PREFIX_DELEGATION 사용 시, 1개의 여유 대역(/28) 유지
}
})
}
}
- 설정 적용
# 모니터링
watch -d kubectl get pod -n kube-system -l k8s-app=aws-node # aws-node 데몬셋 파드 확인
watch -d eksctl get addon --cluster myeks # addon 확인
# 적용
terraform plan
terraform apply -auto-approve

- 확인
# 파드 재생성 확인
kubectl get pod -n kube-system -l k8s-app=aws-node
# addon 확인
eksctl get addon --cluster myeks
NAME VERSION STATUS ISSUES IAMROLE UPDATE AVAILABLE CONFIGURATION VALUES NAMESPACE POD IDENTITY ASSOCIATION ROLES
coredns v1.13.2-eksbuild.3 ACTIVE 0 kube-system
kube-proxy v1.34.5-eksbuild.2 ACTIVE 0 kube-system
vpc-cni v1.21.1-eksbuild.5 ACTIVE 0 {"env":{"MINIMUM_IP_TARGET":"10","WARM_IP_TARGET":"5"}} kube-system
# aws-node DaemonSet의 env 확인
kubectl get ds aws-node -n kube-system -o json | jq '.spec.template.spec.containers[0].env'
kubectl describe ds aws-node -n kube-system | grep -E "WARM_IP_TARGET|MINIMUM_IP_TARGET"
# 노드 정보 확인 : (hostNetwork 제외) 파드가 없는 노드에도 ENI 추가 확인!
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c addr; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done
# cni log 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i tree /var/log/aws-routed-eni ; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo cat /var/log/aws-routed-eni/plugin.log | jq ; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo cat /var/log/aws-routed-eni/ipamd.log | jq ; echo; done
# IpamD debugging commands https://github.com/aws/amazon-vpc-cni-k8s/blob/master/docs/troubleshooting.md
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s http://localhost:61679/v1/enis | jq; echo; done

노드에 파드 생성 갯수 제한
- 사전 준비 : kube-ops-view 설치
# kube-ops-view
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set service.main.type=NodePort,service.main.ports.http.nodePort=30000 --set env.TZ="Asia/Seoul" --namespace kube-system
# 확인
kubectl get deploy,pod,svc,ep -n kube-system -l app.kubernetes.io/instance=kube-ops-view
# kube-ops-view 접속
open "http://$N1:30000/#scale=1.5"
open "http://$N1:30000/#scale=1.3"
[보조 IP] 워커 노드의 인스턴스 정보 확인 : t3.medium 사용 시
# t3 타입의 정보(필터) 확인
aws ec2 describe-instance-types --filters Name=instance-type,Values=t3.\* \
--query "InstanceTypes[].{Type: InstanceType, MaxENI: NetworkInfo.MaximumNetworkInterfaces, IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
--output table
--------------------------------------
| DescribeInstanceTypes |
+----------+----------+--------------+
| IPv4addr | MaxENI | Type |
+----------+----------+--------------+
| 15 | 4 | t3.2xlarge |
| 6 | 3 | t3.medium |
| 12 | 3 | t3.large |
| 15 | 4 | t3.xlarge |
| 2 | 2 | t3.micro |
| 2 | 2 | t3.nano |
| 4 | 3 | t3.small |
+----------+----------+--------------+
# c5 타입의 정보(필터) 확인
aws ec2 describe-instance-types --filters Name=instance-type,Values=c5\*.\* \
--query "InstanceTypes[].{Type: InstanceType, MaxENI: NetworkInfo.MaximumNetworkInterfaces, IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
--output table
10 | 3 | c5.large
# 파드 사용 가능 계산 예시 : aws-node 와 kube-proxy 파드는 host-networking 사용으로 IP 2개 남음
((MaxENI * (IPv4addr-1)) + 2)
t3.medium 경우 : ((3 * (6 - 1) + 2 ) = 17개 >> aws-node 와 kube-proxy 2개 제외하면 15개
# 워커노드 상세 정보 확인 : 노드 상세 정보의 Allocatable 에 pods 에 17개 정보 확인
kubectl describe node | grep Allocatable: -A6
Allocatable:
cpu: 1930m
ephemeral-storage: 27905944324
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 3388360Ki
pods: 17
- [보조 IP] 최대 파드 생성 및 확인
# 워커 노드 3대 EC2 - 모니터링 : 각각 ssh 접속 후
while true; do ip -br -c addr show && echo "--------------" ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; done
# 터미널1
watch -d 'kubectl get pods -o wide'
# 터미널2
## 디플로이먼트 생성
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
EOF
# 파드 확인
kubectl get pod -o wide
kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP
# 파드 증가 테스트 >> 파드 정상 생성 확인, 워커 노드에서 eth, eni 갯수 확인
kubectl scale deployment nginx-deployment --replicas=8
# 파드 증가 테스트 >> 파드 정상 생성 확인, 워커 노드에서 eth, eni 갯수 확인 >> 어떤일이 벌어졌는가?
kubectl scale deployment nginx-deployment --replicas=15
# 파드 증가 테스트 >> 파드 정상 생성 확인, 워커 노드에서 eth, eni 갯수 확인 >> 어떤일이 벌어졌는가?
kubectl scale deployment nginx-deployment --replicas=30
# 파드 증가 테스트 >> 파드 정상 생성 확인, 워커 노드에서 eth, eni 갯수 확인 >> 어떤일이 벌어졌는가?
kubectl scale deployment nginx-deployment --replicas=50
# 파드 생성 실패!
kubectl events
kubectl get pods | grep Pending
nginx-deployment-7fb7fd49b4-d4bk9 0/1 Pending 0 3m37s
nginx-deployment-7fb7fd49b4-qpqbm 0/1 Pending 0 3m37s
...
kubectl describe pod <Pending 파드> | grep Events: -A5
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 45s default-scheduler 0/3 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 2 Too many pods. preemption: 0/3 nodes are available: 1 Preemption is not helpful for scheduling, 2 No preemption victims found for incoming pod.
# cni log 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i tree /var/log/aws-routed-eni; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo cat /var/log/aws-routed-eni/plugin.log | jq ; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo cat /var/log/aws-routed-eni/ipamd.log | jq ; echo; done
"msg": "IP pool stats for network card 0: Total IPs/Prefixes = 15/0, AssignedIPs/CooldownIPs: 15/0, c.maxIPsPerENI = 5"
# IpamD debugging commands https://github.com/aws/amazon-vpc-cni-k8s/blob/master/docs/troubleshooting.md
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s http://localhost:61679/v1/enis | jq; echo; done | grep -E 'node|TotalIPs|AssignedIPs'
>> node 13.124.203.19 <<
"TotalIPs": 15,
"AssignedIPs": 15,
>> node 43.203.146.201 <<
"TotalIPs": 15,
"AssignedIPs": 15,
>> node 3.34.197.113 <<
"TotalIPs": 15,
"AssignedIPs": 15,
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s http://localhost:61679/v1/enis | jq; echo; done
>> node 3.35.209.223 <<
{
"0": {
"TotalIPs": 15,
"AssignedIPs": 15,
"ENIs": {
"eni-075a2211bde1784b7": {
"ID": "eni-075a2211bde1784b7",
"IsPrimary": false,
"IsTrunk": false,
"IsEFA": false,
"DeviceNumber": 2,
"AvailableIPv4Cidrs": {
"192.168.10.18/32": {
"Cidr": {
"IP": "192.168.10.18",
"Mask": "/////w=="
},
"IPAddresses": {
"192.168.10.18": {
"Address": "192.168.10.18",
"IPAMKey": {
"networkName": "aws-cni",
"containerID": "df22039a0ad35994a00b5bb506729c826b6dac8a57c04adbcf5d420c02bb28a9",
"ifName": "eth0"
},
"IPAMMetadata": {
"k8sPodNamespace": "default",
"k8sPodName": "nginx-deployment-54fc99c8d-qpsz6",
"interfacesCount": 1
},
"AssignedTime": "2026-03-18T12:53:36.541368951Z",
"UnassignedTime": "0001-01-01T00:00:00Z"
}
},
"IsPrefix": false,
"AddressFamily": ""
},
...

- 다음 실습을 위해서 디플로이먼트 삭제: kubectl delete deploy nginx-deployment
- maxPods 결정 방법 - Docs , Kor
- 노드에 적용되는 최종 maxPods 값은 특정 우선순위로 상호 작용하는 여러 구성 요소에 따라 달라집니다.
- 우선순위(가장 높은 순서에서 낮은 순서):
- 관리형 노드 그룹 적용 - 사용자 지정 AMI 없이 관리형 노드 그룹을 사용하는 경우 Amazon EKS는 노드 사용자 데이터의 maxPods에 최대 한도를 적용합니다. vCPU가 30개 미만인 인스턴스의 경우 최대 한도는 110 입니다. vCPU가 30개를 초과하는 인스턴스의 경우 최대 한도는 250입니다. 이 값은 maxPodsExpression을 포함하여 다른 maxPods 구성보다 우선합니다.
- ⇒ vCPU 30개 미만 EC2 인스턴스 유형은 (k8s 확장 권고값에 따라) 노드에 최대 파드 110개 제한이 되고, vCPU 30이상 EC2 인스턴스 유형은 (AWS 내부 테스트 권고값에 따라) 노드에 최대 파드 250개 제한을 권고합니다.
- kubelet maxPods 구성 - kubelet 구성에서 직접 maxPods를 설정하는 경우(예: 사용자 지정 AMI를 사용하는 시작 템플릿을 통해) 이 값이 maxPodsExpression보다 우선합니다.
- nodeadm maxPodsExpression - NodeConfig에서 maxPodsExpression을 사용하는 경우 nodeadm은 표현식을 평가하여 maxPods를 계산합니다. 이 방법은 우선순위가 더 높은 소스에 의해 값이 아직 설정되지 않은 경우에만 유효합니다.
- 기본 ENI 기반 계산 - 다른 값이 설정되지 않은 경우 AMI는 인스턴스 유형에서 지원하는 탄력적 네트워크 인터페이스 및 IP 주소 수를 기반으로 maxPods를 계산합니다. 이는 (number of ENIs × (IPs per ENI − 1)) + 2 공식과 동일합니다. + 2는 포드 IP 주소를 소비하지 않는 모든 노드에서 실행되는 Amazon VPC CNI 및 kube-proxy를 고려합니다.
- 관리형 노드 그룹과 자체 관리형 노드 비교
- 사용자 지정 AMI 없이 관리형 노드 그룹을 사용하면 Amazon EKS가 노드의 부트스트랩 사용자 데이터에 maxPods 값을 주입합니다. 이는 다음을 의미합니다.
- maxPods 값은 항상 인스턴스 크기에 따라 110 또는 250으로 제한됩니다.
- 구성한 모든 maxPodsExpression은 이 주입된 값으로 재정의됩니다.
- 다른 maxPods 값을 사용하려면 시작 템플릿에서 사용자 지정 AMI를 지정하고 -use-max-pods false를 -kubelet-extra-args '--max-pods=my-value'와 함께 bootstrap.sh 스크립트로 전달합니다. 예시는 시작 템플릿을 사용한 관리형 노드 사용자 지정 섹션을 참조하세요.
- 자체 관리형 노드를 사용하면 부트스트랩 프로세스를 완벽하게 제어할 수 있습니다. NodeConfig에서 maxPodsExpression을 사용하거나 bootstrap.sh에 --max-pods를 직접 전달할 수 있습니다.
- 사용자 지정 AMI 없이 관리형 노드 그룹을 사용하면 Amazon EKS가 노드의 부트스트랩 사용자 데이터에 maxPods 값을 주입합니다. 이는 다음을 의미합니다.
[IPv4 접두사 위임] Prefix Delegation


- [IPv4 접두사 위임] 설정 - Docs , Workshop
- 사전 확인 - Docs
- To assign IP prefixes to your nodes, your nodes must be AWS Nitro-based. Instances that aren’t Nitro-based continue to allocate individual secondary IP addresses, but have a significantly lower number of IP addresses to assign to Pods than Nitro-based instances do.
- The subnets that your Amazon EKS nodes are in must have sufficient contiguous /28 (for IPv4 clusters) or /80 (for IPv6 clusters) Classless Inter-Domain Routing (CIDR) blocks.
- 사전 확인 - Docs
# 인스턴스 타입 확인 : nitro
aws ec2 describe-instance-types --instance-types t3.medium --query "InstanceTypes[].Hypervisor"
[
"nitro"
]
aws ec2 describe-instance-types --instance-types c5.large --query "InstanceTypes[].Hypervisor"
[
"nitro"
]
eks.tf 수정
# add-on
addons = {
coredns = {
most_recent = true
}
kube-proxy = {
most_recent = true
}
vpc-cni = {
most_recent = true
before_compute = true
configuration_values = jsonencode({
env = {
#WARM_ENI_TARGET = "1" # 현재 ENI 외에 여유 ENI 1개를 항상 확보
#WARM_IP_TARGET = "5" # 현재 사용 중인 IP 외에 여유 IP 5개를 항상 유지, 설정 시 WARM_ENI_TARGET 무시됨
#MINIMUM_IP_TARGET = "10" # 노드 시작 시 최소 확보해야 할 IP 총량 10개
ENABLE_PREFIX_DELEGATION = "true"
#WARM_PREFIX_TARGET = "1" # PREFIX_DELEGATION 사용 시, 1개의 여유 대역(/28) 유지
}
})
}
}
설정 적용
# 모니터링
watch -d kubectl get pod -n kube-system -l k8s-app=aws-node # aws-node 데몬셋 파드 확인
watch -d eksctl get addon --cluster myeks # addon 확인
# 적용
terraform plan
terraform apply -auto-approve
# 기존 파드들도 위 설정 적용을 위해 재기동 해두자!
kubectl rollout restart -n kube-system deployment coredns
kubectl rollout restart -n kube-system deployment kube-ops-view
확인
# 파드 재생성 확인
kubectl get pod -n kube-system -l k8s-app=aws-node
# addon 확인
eksctl get addon --cluster myeks
# aws-node DaemonSet의 env 확인
kubectl get ds aws-node -n kube-system -o json | jq '.spec.template.spec.containers[0].env'
...
{
"name": "ENABLE_PREFIX_DELEGATION",
"value": "true"
},
...
# IPv4 접두사 위임 확인
aws ec2 describe-instances --filters "Name=tag-key,Values=eks:cluster-name" "Name=tag-value,Values=myeks" \
--query 'Reservations[*].Instances[].{InstanceId: InstanceId, Prefixes: NetworkInterfaces[].Ipv4Prefixes[]}' | jq
...
{
"InstanceId": "i-0632ba6cf3551f330",
"Prefixes": [
{
"Ipv4Prefix": "192.168.10.16/28"
},
{
"Ipv4Prefix": "192.168.10.176/28"
}
]
}
...
# cni log 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i tree /var/log/aws-routed-eni; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo cat /var/log/aws-routed-eni/plugin.log | jq ; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo cat /var/log/aws-routed-eni/ipamd.log | jq ; echo; done
# IpamD debugging commands https://github.com/aws/amazon-vpc-cni-k8s/blob/master/docs/troubleshooting.md
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s http://localhost:61679/v1/enis | jq; echo; done

[IPv4 접두사 위임] 최대 파드 생성 및 확인
# 워커 노드 EC2 - 모니터링
while true; do ip -br -c addr show && echo "--------------" ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; done
# 터미널1
watch -d 'kubectl get pods -o wide'
# 터미널2
## 디플로이먼트 생성
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 15
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
EOF
# 파드 확인
kubectl get pod -o wide
kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP
# 파드 증가 테스트 >> 파드 정상 생성 확인, 워커 노드에서 eth, eni 갯수 확인
kubectl scale deployment nginx-deployment --replicas=30
# 파드 증가 테스트 >> 파드 정상 생성 확인, 워커 노드에서 eth, eni 갯수 확인
kubectl scale deployment nginx-deployment --replicas=50
kubectl events
# cni log 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i tree /var/log/aws-routed-eni; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo cat /var/log/aws-routed-eni/plugin.log | jq ; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo cat /var/log/aws-routed-eni/ipamd.log | jq ; echo; done
"msg": "IP stats for Network Card 0 - total IPs: 32, assigned IPs: 15, cooldown IPs: 0"
# IpamD debugging : IP 할당 가능하지만, maxPods 에서 제한됨!
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s http://localhost:61679/v1/enis | jq; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s http://localhost:61679/v1/enis | jq; echo; done | grep -E 'node|TotalIPs|AssignedIPs'
>> node 13.124.203.19 <<
"TotalIPs": 32,
"AssignedIPs": 15,
>> node 43.203.146.201 <<
"TotalIPs": 33,
"AssignedIPs": 15,
>> node 3.34.197.113 <<
"TotalIPs": 33,
"AssignedIPs": 15,
- [IPv4 접두사 위임] kubelet 에 maxPods (임시) 수정 후 최대 파드 생성 시도(110대)
- 모니터링
while true; do kubectl describe node -l tier=primary | grep pods | uniq ; sleep 1; done
while true; do kubectl get pod | grep Pending | wc -l ; sleep 1; done
- 워커 노드 3대 각각 접속 후 maxPods (임시) 수정
# 기본 정보 확인
cat /etc/kubernetes/kubelet/config.json | grep maxPods
cat /etc/kubernetes/kubelet/config.json.d/40-nodeadm.conf | grep maxPods
# sed 로 변경 : 기존 17 -> 변경 40
sudo sed -i 's/"maxPods": 17/"maxPods": 50/g' /etc/kubernetes/kubelet/config.json
sudo sed -i 's/"maxPods": 17/"maxPods": 50/g' /etc/kubernetes/kubelet/config.json.d/40-nodeadm.conf
# 적용
sudo systemctl restart kubelet
- 추가로 증가
# 현재 파드 갯수
kubectl get pod -l app=nginx --no-headers=true | wc -l
50
# 파드 증가 테스트 >> 파드 정상 생성 확인, 워커 노드에서 eth, eni 갯수 확인
kubectl scale deployment nginx-deployment --replicas=60
# 파드 증가 테스트 >> 파드 정상 생성 확인, 워커 노드에서 eth, eni 갯수 확인
kubectl scale deployment nginx-deployment --replicas=90
# 파드 증가 테스트 >> 파드 정상 생성 확인, 워커 노드에서 eth, eni 갯수 확인
kubectl scale deployment nginx-deployment --replicas=110
# 파드 확인
kubectl events
kubectl get pod -o wide
kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP
- 확인
# cni log 확인 : maxPods 는 가능하지만, IP 할당 실패!
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i tree /var/log/aws-routed-eni; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo cat /var/log/aws-routed-eni/plugin.log | jq ; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo cat /var/log/aws-routed-eni/ipamd.log | jq ; echo; done
"msg": "UnassignPodIPAddress: IP address pool stats: total 33, assigned 32, sandbox aws-cni/c38e2c6b31ce0c6f332bc1f52d9189c703f18cb2daa68bfb50ab57e341ad4397/eth0"
# IpamD debugging commands https://github.com/aws/amazon-vpc-cni-k8s/blob/master/docs/troubleshooting.md
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s http://localhost:61679/v1/enis | jq; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s http://localhost:61679/v1/enis | jq; echo; done | grep -E 'node|TotalIPs|AssignedIPs'
>> node 13.124.203.19 <<
"TotalIPs": 32,
"AssignedIPs": 32,
>> node 43.203.146.201 <<
"TotalIPs": 33,
"AssignedIPs": 32,
>> node 3.34.197.113 <<
"TotalIPs": 33,
"AssignedIPs": 32,
# 노드 정보 확인 : maxPods 가 50으로 이번에는 Prefix mode 에서 최대 IP 할당 후에도 IP 부족!
kubectl describe node -l tier=primary | grep pods
kubectl describe node -l tier=primary
...
Capacity:
cpu: 2
ephemeral-storage: 20893676Ki
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 3926448Ki
pods: 50
Allocatable:
cpu: 1930m
ephemeral-storage: 18181869946
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 3371440Ki
pods: 50
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
...
Normal NodeAllocatableEnforced 63s kubelet Updated Node Allocatable limit across pods'Kubernetes' 카테고리의 다른 글
| AEWS 4기 | 1주차 #2 EKS Cluster Endpoint Access (0) | 2026.03.17 |
|---|---|
| AEWS 4기 | 1주차 #1 EKS 구성 방식 및 실습 환경 (1) | 2026.03.16 |
| Kubernetes CI/CD Study 1기 | 7주차 #1 HashiCorp Vault (0) | 2025.11.27 |
| Kubernetes CI/CD Study 1기 | 6주차 ArgoCD 3/3 (0) | 2025.11.21 |
| Kubernetes CI/CD Study 1기 | 5주차 ArgoCD 2/3 (0) | 2025.11.10 |