EKS Networking
Kubernetes CLI
스터디 도중 노션에 있던 명령어들과, 실습에 사용했던 명령어들을 정리해 보았다.
일종의 치트시트로 필요할때 빠르게 찾기 위해 위쪽에 두었다!
세부적인 네트워크 관련 내용은 다음 부분부터 시작한다.
EKS 기본정보 확인
kubectl cluster-info: 클러스터 정보 확인eksctl get cluster: 현재 AWS 계정에서 생성된 EKS 클러스터 목록 조회kubens default: 현재 컨텍스트의 기본 네임스페이스를default로 전환 >kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone: 노드 정보 확인kubectl get node -v=6: 노드 목록 조회 (verbosity 6 — API 요청/응답 URL 포함한 상세 로그 출력)kubectl get node --show-labels: 노드에 부착된 모든 레이블 표시kubectl get node -l tier=primary:tier=primary레이블이 있는 노드만 필터링하여 조회kubectl get pod -A: 전체 네임스페이스의 파드 목록 조회kubectl get pdb -n kube-system:kube-system네임스페이스의 PodDisruptionBudget 목록 조회 (노드 드레인 시 중단 허용 범위 정책)aws eks describe-nodegroup --cluster-name myeks --nodegroup-name myeks-1nd-node-group | jq: 특정 노드 그룹의 상세 정보 조회 (인스턴스 타입, 스케일링 설정, AMI 등)aws eks list-addons --cluster-name myeks | jq: 클러스터에 설치된 EKS 관리형 애드온 목록 조회 (vpc-cni, coredns, kube-proxy 등)eksctl get addon --cluster myeks: eksctl로 애드온 목록 및 버전/상태 조회
aws-node / kube-proxy 확인
kubectl get daemonset aws-node -n kube-system -owide: aws-node DaemonSet 상태 확인kubectl describe daemonset aws-node -n kube-system: aws-node DaemonSet 상세 정보kubectl get ds aws-node -n kube-system -o json | jq '.spec.template.spec.containers[0].env': aws-node 환경변수 확인 (WARM_ENI_TARGET 등 VPC CNI 설정값)kubectl describe cm -n kube-system kube-proxy-config | grep mode: kube-proxy 모드 확인 (iptables / IPVS)kubectl describe cm -n kube-system kube-proxy-config | grep iptables: -A5: kube-proxy iptables 세부 설정 확인
노드 / 파드 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: 실행 중인 EC2 노드의 Public/Private IP 확인kubectl get pod -n kube-system -o=custom-columns=NAME:.metadata.name,IP:.status.podIP,STATUS:.status.phase: 파드별 IP와 상태를 컬럼으로 출력kubectl get pod -A -o name: 전체 네임스페이스 파드 이름만 출력kubectl get pod -A -o name | wc -l: 전체 파드 갯수 확인
k9s를 활용한 방법
k9s는 터미널 기반 Kubernetes UI로, kubectl 명령어를 일일이 입력하지 않고 대화형으로 클러스터를 탐색할 수 있다.
물론 kubectl명령어를 사용하는것도 중요하지만, 나는 k9s를 이용하여 탐색하는것을 더 선호한다. 위의 모든 명령어를 간단하게 활용하는것이 가능.
기본 사용법
k9s # 현재 컨텍스트로 실행
k9s --context myeks # 특정 컨텍스트로 실행
k9s -n kube-system # 특정 네임스페이스로 시작
실습환경
실습용으로 받은 업스트림의 terraform code를 일부 리팩터링 하여두었다.
## 환경변수 선언
배포 전 아래 두 변수를 반드시 export 해야 합니다.
1. 현재 IP를 SSH 허용 대역으로 설정
`export TF_VAR_ssh_access_cidr=$(curl -s ipinfo.io/ip)/32`
2. AWS에 등록된 EC2 키페어 이름 (aws ec2 import-key-pair 로 등록)
`export TF_VAR_key_name=<your-key-pair-name>`
아니면 variables에서 기본값을 원하는거로 변경하세요.
3. 두번째 노드를 활성화 하고 싶은 경우
`export TF_VAR_enable_second_node=true`
## 배포
1. `tofu init`
2. `tofu plan`: 이 단계에서 변경사항 꼭 다 살펴보기!
2. `tofu apply`: 과금시작 :)
Terraform Code와 함께 보는 EKS Networking
아무래도 엔지니어 입장에서는 설명을 듣는거보다 소스코드를 보면서 직접 생각을 하는것이 훨씬 이해하기 쉬운거 같다.
코드를 보면서 내가 생각한 것을 추가로 적어보았다.
VPC, EKS 애드온, VPC CNI 플러그인
VPC 구성
# vpc.tofu
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~>6.6"
name = "${var.cluster_base_name}-VPC"
cidr = var.vpc_cidr # "192.168.0.0/16"
azs = var.availability_zones # ["eu-west-2a", "eu-west-2b", "eu-west-2c"]
# EKS 내부 DNS(CoreDNS)가 이 설정에 의존함 - 비활성화 시 Pod간 통신 불가
enable_dns_support = true
enable_dns_hostnames = true
public_subnets = var.public_subnet_blocks # ["192.168.0.0/22", "192.168.4.0/22", "192.168.8.0/22"]
private_subnets = var.private_subnet_blocks # ["192.168.12.0/22", "192.168.16.0/22", "192.168.20.0/22"]
enable_nat_gateway = false # 실습환경이라 NAT 없이 public subnet에 노드 배치
manage_default_network_acl = false # 기본 ACL(전체허용) 사용
map_public_ip_on_launch = true # public subnet 노드에 공인 IP 자동 부여
# EKS가 LoadBalancer Service 생성 시, 이 태그로 서브넷을 자동 탐색함
public_subnet_tags = { "kubernetes.io/role/elb" = "1" }
private_subnet_tags = { "kubernetes.io/role/internal-elb" = "1" }
}
서브넷을 /22로 잡으면 서브넷당 사용 가능한 IP가 1,019개가 된다.
EKS에서 Pod IP는 VPC IP를 직접 소비하고 subnet을 한번 설정한 상태에서는 변경하기가 어렵기 때문에 처음 클러스터를 구축할때부터 충분한 ip를 할당해 두어야 한다.
EKS 클러스터 애드온(Add-on)
# eks.tofu
addons = {
coredns = { most_recent = true } # 클러스터 내부 DNS
kube-proxy = { most_recent = true } # 노드별 네트워크 룰(iptables/IPVS) 관리
vpc-cni = {
most_recent = true
before_compute = true # 노드가 뜨기 전에 CNI를 먼저 설치 (중요!)
configuration_values = jsonencode({
env = {
WARM_ENI_TARGET = "1"
# WARM_IP_TARGET = "5"
# MINIMUM_IP_TARGET = "10"
# ENABLE_PREFIX_DELEGATION = "true"
# WARM_PREFIX_TARGET = "1"
}
})
}
}
| 애드온 | 역할 |
|---|---|
| coredns | Pod 내부에서 서비스명.네임스페이스.svc.cluster.local 형태의 DNS 질의 처리 |
| kube-proxy | 각 노드에서 Service -> Pod IP 매핑을 위한 iptables/IPVS 룰 관리 |
| vpc-cni | Pod에 VPC IP를 직접 할당. EKS 네트워킹의 핵심 |
kube-proxy의 IPVS 모드는 사용하지 않는 것을 권장한다. Kubernetes 1.35부터 공식 deprecated 예정이며, iptables 대비 실질적인 성능 이점이 없다.
before_compute = true가 중요한 이유: 노드가 준비되기 전에 CNI가 없으면 Pod 네트워크가 구성되지 않아 노드가 NotReady 상태로 멈춘다.
Amazon VPC CNI 플러그인
VPC CNI란?
일반적인 Kubernetes CNI(Flannel, Calico 등)는 노드 네트워크와 Pod 네트워크를 분리한다. 반면 Amazon VPC CNI는 Pod에 VPC의 실제 IP를 직접 부여한다.
일반 CNI: 노드IP(172.16.0.1) -- overlay --> Pod IP(10.244.0.5) # VPC에서는 모름
VPC CNI: 노드IP(192.168.0.10) Pod IP(192.168.0.15) # VPC에서 직접 라우팅 가능
overlay: flannel, Calico 같은 CNI가 사용하는 가상 네트워크를 말한다. Pod 간 패킷을 노드 IP로 감싸서(encapsulation) 전달하는 방식이라, AWS VPC 입장에서는 내부 Pod IP를 알 수 없다. EKS에서는 VPC CNI가 기본이고, overlay가 없으므로 encapsulation 오버헤드도 없다.
이 덕분에 Pod - RDS, Pod - ElastiCache 등 AWS 서비스 간 통신 시 추가 오버헤드 없이 VPC 라우팅 테이블로 직접 연결된다.
VPC CNI는 노드에 aws-node라는 DaemonSet으로 배포된다.
kubectl get daemonset -n kube-system aws-node
노드당 최대 Pod 수 (Secondary IP 모드)
Pod IP는 노드에 붙은 ENI(Elastic Network Interface)의 보조 IP 주소에서 소비된다. 따라서 최대 Pod 수는 인스턴스 타입의 ENI/IP 한도에 묶인다.
최대 Pod 수 = (ENI 수 × (ENI당 IP 수 - 1)) + 2
이 실습에서는 t3.medium을 사용한다 (variables.tofu의 worker_node_instance_type).
| 항목 | t3.medium 기준 |
|---|---|
| 최대 ENI 수 | 3 |
| ENI당 최대 IP | 6 |
| 최대 Pod 수 | (3 x (6-1)) + 2 = 17개 |
확인 방법:
kubectl get node -o jsonpath='{.items[*].status.allocatable.pods}'
VPC CNI IP 할당 모드 3가지
용어 정리
ipamd (IP Address Management Daemon)
aws-node DaemonSet 안에서 실행되는 백그라운드 프로세스다. 하는 일은 크게 두 가지다.
- EC2 API를 호출해 노드에 ENI를 붙이고 IP를 확보한다
- Kubelet이 Pod를 스케줄하면 확보해둔 IP를 즉시 넘겨준다
Warm Pool
ipamd가 미리 확보해두는 ENI/IP 재고다. Pod가 뜰 때마다 EC2 API를 호출하면 IP 하나 받는 데 수 초가 걸린다.
Warm Pool은 이 지연을 없애기 위해 "쓸 IP를 항상 조금씩 미리 사둔다"는 개념으로(일종의 캐시?)
WARM_ENI_TARGET / WARM_IP_TARGET이 이 재고를 얼마나 유지할지 결정하는 설정이다.
1. Secondary IP 모드 (기본값)
VPC CNI의 기본 동작 방식이다.
노드가 시작되면 기본 ENI(Primary ENI)가 부착되고, ipamd가 보조 ENI와 IP를 미리 확보해 Warm Pool을 만들어둔다. Pod가 생성되면 이 풀에서 즉시 IP를 할당한다.
- 장점: 구성이 단순하고 Pod IP 할당이 빠름
- 단점: 노드당 Pod 수가 인스턴스 ENI/IP 한도에 묶임.
t3.medium은 17개가 최대
2. IPv4 Prefix Delegation 모드
ENI에 개별 IP 대신 /28 대역(16개 IP)을 통째로 위임받는 방식이다.
# 활성화 방법 (eks.tofu의 vpc-cni 설정)
ENABLE_PREFIX_DELEGATION = "true"
WARM_PREFIX_TARGET = "1" # 여유 /28 블록 1개 유지
| 항목 | t3.medium 기준 |
|---|---|
| 최대 ENI 수 | 3 |
| ENI당 최대 Prefix | 5 |
| 최대 Pod 수 | (3 x 5 x 16) - 몇 개 = ~240개 |
- 장점: 같은 인스턴스로 훨씬 더 많은 Pod 수용 가능
- 단점: VPC 서브넷이
/28정렬 주소를 충분히 가지고 있어야 함. 서브넷이 작으면 Prefix를 할당받지 못할 수 있음
3. Custom Networking (노드/Pod 대역 분리)
노드 IP와 Pod IP를 서로 다른 서브넷에서 가져오는 방식이다.
노드: public subnet (192.168.0.0/22)에 배치
Pod: private subnet (192.168.12.0/22)에서 IP 할당
- 장점: 노드 서브넷 IP를 Pod가 소모하지 않음. 노드/Pod 보안 정책을 서브넷 레벨로 분리 가능
- 단점: ENIConfig 리소스를 추가로 관리해야 하고 구성이 복잡함
WARM_ENI_TARGET / WARM_IP_TARGET / MINIMUM_IP_TARGET 상세
이 실습 코드에는 현재 WARM_ENI_TARGET = "1"만 활성화되어 있다.
나머지는 주석 처리되어 있고, 세 변수가 어떻게 다른지 비교해본다.
# eks.tofu - vpc-cni 설정
configuration_values = jsonencode({
env = {
WARM_ENI_TARGET = "1" # 현재 활성화
# WARM_IP_TARGET = "5"
# MINIMUM_IP_TARGET = "10"
# ENABLE_PREFIX_DELEGATION = "true"
# WARM_PREFIX_TARGET = "1" # PREFIX_DELEGATION 사용 시, 1개의 여유 대역(/28) 유지
}
})
WARM_ENI_TARGET
ENI-1이 사용 중일 때 ENI-2를 미리 비워둔 상태로 유지한다. ENI-2도 차면 ipamd가 ENI-3을 자동 부착한다.
- 단위: ENI 개수
- 특징: ENI 단위로 확보하므로 IP를 많이 선점. 빠르지만 낭비 발생 가능
- 주의:
WARM_IP_TARGET을 설정하면 이 값은 무시됨
WARM_IP_TARGET
Pod 10개 실행 중이라면 여유 IP 5개를 추가로 유지해 총 15개 IP를 확보한 상태로 유지한다.
- 단위: IP 개수
- 특징: ENI보다 세밀하게 관리. IP가 부족한 VPC 환경에 적합
- 설정 시:
WARM_ENI_TARGET을 덮어씀
MINIMUM_IP_TARGET
Pod가 0개여도 노드 시작 시점에 IP를 지정한 수만큼 미리 선점한다.
- 단위: IP 총량
- 특징: 초기 급격한 Pod 스케일아웃
- 보통
WARM_IP_TARGET과 함께 사용
ENABLE_PREFIX_DELEGATION
ENI에 개별 IP 대신 /28 CIDR 블록(16개 IP) 단위로 할당하는 모드를 활성화한다.
기본 모드에서는 ENI당 IP를 1개씩 붙이지만, 이 옵션을 켜면 prefix(대역) 단위로 한번에 붙인다.
- 단위: /28 prefix (IP 16개)
- 특징: 노드당 할당 가능한 Pod 수가 크게 증가. 고밀도 Pod 배치에 유리
- 주의: Nitro 기반 인스턴스에서만 지원됨.
WARM_IP_TARGET/WARM_PREFIX_TARGET과 함께 사용
WARM_PREFIX_TARGET
ENABLE_PREFIX_DELEGATION = "true" 활성화 시 사용하는 변수로, 미리 비워둘 /28 prefix 개수를 지정한다.
"1" 설정 시 현재 사용 중인 prefix가 찰 때 /28 대역 1개를 여유분으로 미리 확보해둔다.
- 단위: /28 prefix 개수
- 특징:
WARM_IP_TARGET대신 prefix 단위로 여유분을 관리. prefix 모드의WARM_ENI_TARGET역할 - 주의:
ENABLE_PREFIX_DELEGATION이 비활성화된 상태에서는 무시됨
변수 비교 요약
| 변수 | 단위 | 역할 | 주 사용 시나리오 |
|---|---|---|---|
WARM_ENI_TARGET |
ENI 개수 | 빈 ENI를 N개 미리 부착 | 기본 설정, 빠른 IP 확보 |
WARM_IP_TARGET |
IP 개수 | 여유 IP를 N개 유지 | IP가 부족한 VPC |
MINIMUM_IP_TARGET |
IP 총량 | 노드 시작 시 최소 N개 선점 | 대규모 초기 스케일아웃 |
ENABLE_PREFIX_DELEGATION |
- (플래그) | ENI에 /28 prefix 단위로 IP 할당 활성화 | 고밀도 Pod 배치, IP 효율 극대화 |
WARM_PREFIX_TARGET |
prefix 개수 | 여유 /28 prefix를 N개 유지 | prefix 모드에서 빠른 IP 확보 |
For more information: - VPC CNI Best Practices - ENI and IP Target 상세
노드 당 최대 파드 수
노드 하나에 파드를 몇 개까지 올릴 수 있는지를 결정하는 값으로 크게 다음 두 가지라고 생각된다.
- IP 수 (ENI 구조에서 오는 물리적 한도)
- maxPods (kubelet에서 설정)
두 값 중 더 작은 쪽이 실제 제한이 된다.
기본 공식
최대 파드 수 = (ENI 수 × (ENI당 IP 수 - 1)) + 2
-1은 ENI의 기본 IP(노드 IP)를 Pod에 쓸 수 없기 때문이다.
+2는 aws-node와 kube-proxy가 노드 IP를 직접 쓰기 때문에 Pod IP 소모 없이 추가로 인정된다.
t3.medium 예시:
| 항목 | 값 |
|---|---|
| ENI 수 | 3 |
| ENI당 IP | 6 |
| 계산 | (3 × (6-1)) + 2 = 17개 |
maxPods 우선순위
EKS는 아래 우선순위로 maxPods를 결정한다. 높은 순서일수록 우선 적용된다.
| 우선순위 | 방식 | 설명 |
|---|---|---|
| 1 | 관리형 노드 그룹 기본값 | EKS가 자동 주입. vCPU < 30이면 110, ≥ 30이면 250으로 강제 제한 |
| 2 | kubelet 직접 설정 | 사용자 지정 AMI + 시작 템플릿에서 직접 지정 시 |
| 3 | nodeadm maxPodsExpression | nodeadm 설정에서 수식으로 계산 |
| 4 | ENI 기반 자동 계산 | 위 값이 없을 때 공식으로 계산 |
즉, 일반적인 관리형 노드 그룹 환경에서는 ENI 공식보다 EKS가 주입한 110/250이 우선 적용된다.
t3.medium은 ENI 공식으로는 17개지만, EKS가 110으로 올려준다.
Prefix Delegation과의 관계
Prefix Delegation을 켜면 노드에 할당되는 IP 수는 크게 늘어난다. 하지만 IP가 많아졌다고 해서 파드를 그만큼 더 올릴 수 있는 건 아니다.
Prefix Delegation의 효과를 실제로 보려면 kubelet maxPods도 함께 올려야 한다.
관리형 노드 그룹에서는 EKS가 maxPods를 강제 주입하므로, 이를 바꾸려면 사용자 지정 AMI가 필요하다.
Service & EKS 네트워킹
kube-proxy - iptables 룰 관리
# eks.tofu
addons = {
kube-proxy = {
most_recent = true
}
# coredns, vpc-cni 생략
}
kube-proxy는 각 노드에 DaemonSet으로 떠서 Service IP를 실제 Pod IP로 연결하는 iptables 룰을 관리한다. 직접 트래픽을 받아서 처리하는 게 아니라, 커널 netfilter에 규칙을 등록하고 빠진다. 트래픽 처리 자체는 커널이 한다.
Service를 하나 만들면 kube-proxy가 모든 노드에 동일한 iptables 규칙을 심는다. 그래서 어떤 노드에서 ClusterIP로 보내도 Pod까지 도달한다.
iptables 모드에서 ClusterIP 트래픽 흐름
PREROUTING -> KUBE-SERVICES -> KUBE-SVC-ㅇㅇㅇ -> KUBE-SEP-ㅇㅇㅇ (DNAT -> Pod IP)
파드가 3개라면 각 KUBE-SEP으로 분기하는 확률이 순서대로 33% / 50% / 100%로 설정된다. 첫 번째 룰에서 통과 못하면 남은 2개 중 하나를 고르게 되니, 결과적으로 각 파드에 1/3씩 분산된다.
KUBE-SVC-### 체인 예시 (파드 3개)
-> KUBE-SEP-1 probability 0.33333 (33%)
-> KUBE-SEP-2 probability 0.50000 (남은 트래픽의 50% = 전체 33%)
-> KUBE-SEP-3 (나머지 전부 = 전체 33%)
DNAT 이후 POSTROUTING 단계에서는 SNAT이 적용되지 않는다. 클러스터 내부에서 ClusterIP로 보낸 트래픽은 출발지 IP가 그대로 Pod에 도달한다.
kube-proxy 모드 종류
| 모드 | 동작 방식 | 비고 |
|---|---|---|
| iptables | netfilter에 iptables 규칙 등록. 기본값 | 안정적. EKS 기본값 |
| IPVS | 커널 IPVS + 해시 테이블 기반 | K8s 1.35에서 deprecated 예정 |
| nftables | iptables 후계자. kernel 5.13 이상 필요 | 비교적 신규, 플러그인 호환성 확인 필요 |
| eBPF | netfilter 우회, 커널에서 직접 패킷 처리 | Cilium 같은 별도 CNI가 kube-proxy 대체 |
IPVS가 iptables보다 빠르다는 말이 있지만, 일반 규모에서는 체감 차이가 거의 없고 복잡성만 올라간다.
Service 종류와 보안 그룹
ClusterIP
클러스터 내부에서만 접근 가능한 가상 IP다. 외부에서 직접 붙을 수 없고, 서비스 간 내부 통신에 쓴다. 실제 IP를 가진 게 아니라 iptables 룰로만 존재하는 가상 주소다.
NodePort
# eks.tofu - 노드 보안 그룹 규칙
resource "aws_security_group_rule" "allow_all_for_my_ip" {
type = "ingress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [
var.ssh_access_cidr, # 내 IP
var.vpc_cidr # VPC 내부
]
security_group_id = aws_security_group.node_group_sg.id
}
ClusterIP에 더해 각 노드의 특정 포트(30000-32767)를 열어준다. 트래픽 경로: 외부 -> 노드IP:NodePort -> KUBE-SVC -> Pod
NodePort는 간단하지만, 노드 IP가 직접 노출된다. 실제 서비스에는 앞단에 로드밸런서를 두는 게 일반적이다.
LoadBalancer
NodePort에 더해 AWS 로드밸런서를 자동으로 프로비저닝한다. 어떤 컨트롤러를 쓰느냐에 따라 트래픽 경로가 달라진다.
인스턴스 모드: 외부 -> NLB -> 노드IP:NodePort -> Pod
Pod IP 모드: 외부 -> NLB -> Pod IP 직접 (홉 1개 절약)
Pod IP 모드는 AWS Load Balancer Controller를 별도로 설치해야 하고, EKS + VPC CNI 조합에서만 가능하다. VPC CNI가 Pod에 VPC IP를 직접 부여하기 때문에 NLB가 Pod IP로 바로 트래픽을 보낼 수 있는 것이다.
LoadBalancer와 서브넷 태그
LoadBalancer 타입 Service를 만들면 EKS가 VPC에서 적절한 서브넷을 찾아 로드밸런서를 배치한다. 이때 서브넷을 찾는 기준이 태그다.
# vpc.tofu
public_subnet_tags = {
"kubernetes.io/role/elb" = "1" # 인터넷 facing LB용 서브넷
}
private_subnet_tags = {
"kubernetes.io/role/internal-elb" = "1" # 내부 LB용 서브넷
}
이 태그가 없으면 EKS가 어느 서브넷에 로드밸런서를 만들지 판단하지 못해서 Service 생성이 실패한다. 외부 노출용 LB는 public 서브넷에, 내부 통신용 LB는 private 서브넷에 각각 배치된다.
이 실습 코드는 enable_nat_gateway = false로 NAT 없이 노드를 public 서브넷에 직접 두었기 때문에,
실제로는 external LB만 사용하게 된다. private 서브넷의 태그는 NAT를 쓰는 환경에서 의미가 있다.