목차
1. AWS VPC CNI 소개
2. 노드에서 기본 네트워크 정보 확인
3. 노드 간 파드 통신
4. 파드에서 외부 통신
5. AWS VPC CNI 설정 변경
6. 노드에 파드 생성 갯수 제한
7. Service & Amazon EKS 네트워킹 지원
8. AWS LoadBalancer Controller (LBC) & Service (L4)
9. Ingress (L7 : HTTP)
10. ExternalDNS
11. Gateway API
12. CoreDNS
CNI(Container Network Interface) 는 Kubernetes에서 Pod 네트워크를 구성하는 표준 플러그인 인터페이스입니다. Pod가 생성될 때 "이 Pod에 어떻게 IP를 주고, 어떻게 통신하게 할 것인가"를 담당하는 컴포넌트입니다.
그 중 AWS VPC CNI는 EKS(Amazon Elastic Kubernetes Service)에서 기본으로 사용하는 CNI 플러그인으로, Pod에 VPC의 실제 IP 주소를 직접 할당하는 방식으로 동작합니다.
결과적으로 Pod IP = VPC IP 가 되는 구조입니다.
저희회사의 경우는 하이브리드 클라우드인데 실제로 프라이빗에 구축해놓은 쿠버네티스 환경은
개발환경기준으로 노드는 192. 대역을 사용하고, pod는 10. 대역을 사용하고 있습니다.
Amazon EKS는 VPC CNI 플러그인을 통해 클러스터 네트워킹을 구현
이 플러그인을 사용하면 파드가 VPC 네트워크와 동일한 IP 대역을 직접 사용할 수 있습니다.
일반 K8S CNI(Calico 등)는 VXLAN 같은 오버레이 방식으로 파드 간 통신을 하지만, VPC CNI는 오버레이 없이 VPC IP로 직접 통신
같은 파드 내 컨테이너들은 네트워크 네임스페이스를 공유하며 로컬 포트로 서로 통신합니다.
구성 요소

IPAM 환경변수 비교

# 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

# 워커 노드 SSH 접속
for i in $N1 $N2 $N3; do
echo ">> node $i <<"
ssh -i ~/.ssh/my-keypair.pem -o StrictHostKeyChecking=no ec2-user@$i hostname
echo
done

kube-proxy config 확인 : 모드 iptables 사용
▫️ nftables로 가야 하나?
iptables는 서비스 수가 늘어날수록 O(n) 방식으로 처리 시간이 증가하지만, nftables는 O(1) map lookup 구조라 클러스터 규모에 관계없이 처리 시간이 일정
▫️ipvs는 디프리케이트 예정
kubectl describe cm -n kube-system kube-proxy-config
Name: kube-proxy-config
Namespace: kube-system
Labels: eks.amazonaws.com/component=kube-proxy
k8s-app=kube-proxy
Annotations: <none>
Data
====
config:
----
apiVersion: kubeproxy.config.k8s.io/v1alpha1
...
iptables:
masqueradeAll: false
masqueradeBit: 14
minSyncPeriod: 0s
syncPeriod: 30s
ipvs:
excludeCIDRs: null
minSyncPeriod: 0s
scheduler: ""
syncPeriod: 30s
kind: KubeProxyConfiguration
metricsBindAddress: 0.0.0.0:10249
mode: "iptables"
...
노드와 pod ip 확인
# 노드 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 확인
kubectl get pod -n kube-system -o=custom-columns=NAME:.metadata.name,IP:.status.podIP,STATUS:.status.phase


-> 노드와 pod 같은 192.168 ip 대역을 사용중
오버레이 없이 VPC Native하게 파드 간 직접 통신 가능
→ 파드가 VPC CIDR의 실제 IP를 직접 할당받기 때문

-> default ns 에 netshoot pod 가 각 node에 배포가 되었는데 하나의 pod에서 다른 pod로 통신을 어떻게 하는지 알아보는 단계

ping 통신도 됨!
tcpdump도 떠 본다면 노드의 ip로 오버레이 되는거 없이 pod 의 ip 로 통신되는것을 확인할 수 있었습니다
▫️ pod1 에서 google로 ping 테스트를 해보았다
pod ip가 아닌 노드 ip 로 nat되어 통신되는 것을 확인 할 수 있다(192.168.6.6)

▫️ pod도 외부랑 통신 할 땐 node와 마찬가지로 공인ip를 사용하는것을 아래 사진을 통해 확인할 수 있다

▫️ 현재상태: WARM_ENI_TARGET = "1" (현재 ENI 외에 여유 ENI 1개를 항상 확보)
vpc-cni = {
most_recent = true
before_compute = true
configuration_values = jsonencode({
env = {
WARM_ENI_TARGET = "1"
}
})
}
▫️ 변경하고자 하는 것: eni 기반이 아닌 ip기반이 되는 것
WARM_IP_TARGET = "5" # 현재 사용 중인 IP 외에 여유 IP 5개를 항상 유지, 설정 시 WARM_ENI_TARGET 무시됨
MINIMUM_IP_TARGET = "10" # 노드 시작 시 최소 확보해야 할 IP 총량 10개
vpc-cni = {
most_recent = true
before_compute = true
configuration_values = jsonencode({
env = {
WARM_IP_TARGET = "5"
MINIMUM_IP_TARGET = "10"
}
})
}
▫️ vpc cni 설정을 변경하면 aws-node 데몬셋 파드가 리프레시 됨
▫️ 웹콘솔 내 add-on(vpc-cni) 내 정보도 변경된 것을 확인할 수 있습니다

-> WARM_ENI_TARGET 모드일 땐 core-dns가 없는 node에는 eni 인터페이스가 없었는데
MINIMUM_IP_TARGET 설정이 10이 되면서 각각 노드에 eni 가 미리? 할당된 것도 확인 가능하다.
Secondary IPv4 addresses (기본값) : 인스턴스 유형에 최대 ENI 갯수와 할당 가능 IP 수를 조합하여 선정
최대 파드 생성 갯수 :
(Number of network interfaces for the instance type × (the number of IP addressess per network interface - 1)) + 2
▫️공식에 의해 t3.medium은 node 당 17개의 pod를 가질 수 있다.


▫️ 간단하게 3개의 nginx pod를 배포해본다

## 디플로이먼트 생성
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

무난하게 잘 늘어난다
replicas 를 30개 까지 늘려봐도 소화가 됨

replicas 50개는 소화를 못한다

deployment에 pending된 pod 로그를 확인해보면 too many pod 에러 로그를 확인해볼 수 있다.

우선순위

cni 부분 수정
vpc-cni = {
most_recent = true
before_compute = true
configuration_values = jsonencode({
env = {
ENABLE_PREFIX_DELEGATION = "true"
}
})
}
IPv4 접두사 위임 확인
ipv4 prefix 가 접두사로 /28 bit subnet이 추가된 것을 확인 가능

웹콘솔에서도 확인해볼 수 있다

🔺 nginx pod를 50개 배포해봤다

그런데 pod생성을 늘려놨는데 아까랑 비슷하다?
그렇지만 에러 내용은 다르다

pod가 pending 되는 이유는 kubelet 의 maxpod에서 걸린다
각 노드에 접속해서 max pod개수를 17개에서 50개로 늘려본다

# 기본 정보 확인
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
node 3개에 전부 적용했다
50개의 nginx pod가 전부 node에 배치되었다

한번에 110으로 증설해보겠다

pod ip를 최대로 사용한것을 확인할 수 있다.

▫️ 지금 설정은 노드가 재기동되면 리셋된다. 시작템플릿에서 userdata에서 수정해줘야 리셋되지않고 사용 가능하니 참고하자

eks.tf 를 아래와 같이 수정해보겠습니다
# AL2023 전용 userdata 주입
cloudinit_pre_nodeadm = [
{
content_type = "application/node.eks.aws"
content = <<-EOT
---
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
kubelet:
config:
maxPods: 50
EOT
},
{
content_type = "text/x-shellscript"
content = <<-EOT
#!/bin/bash
echo "Starting custom initialization..."
dnf update -y
dnf install -y tree bind-utils tcpdump nvme-cli links sysstat ipset htop
echo "Custom initialization completed."
EOT
}
]

바꿔봅니다..
# 1. kubelet 설정에서 maxPods 확인
sudo cat /etc/kubernetes/kubelet/config.json | grep maxPods
# 2. nodeadm 설정 확인
sudo cat /etc/kubernetes/kubelet/config.json.d/40-nodeadm.conf | grep maxPods

뭔가... 바뀌다 만 느낌입니다..8ㅅ8 너무 졸려서 다음에 기회가 되면 수정하겠습니다...
Service가 필요한 이유
Pod는 죽었다 살아날 때마다 IP가 바뀝니다. Service는 이 불안정한 Pod 앞에 고정된 엔드포인트를 제공합니다.
ClusterIP
클러스터 내부에서만 접근 가능한 가상 IP를 부여합니다. Pod끼리 통신할 때 사용합니다.
yamlspec:
type: ClusterIP
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
NodePort
각 노드의 특정 포트(30000~32767)를 열어 외부에서 접근 가능하게 합니다. EKS에서는 잘 쓰지 않습니다.
LoadBalancer
EKS에서 가장 많이 씁니다. AWS Load Balancer Controller가 자동으로 NLB 또는 CLB를 생성합니다.
yamlspec:
type: LoadBalancer
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
AWS Load Balancer Controller + Annotation으로 NLB/ALB를 세밀하게 제어할 수 있습니다.
ExternalName
클러스터 내부 DNS를 외부 도메인으로 CNAME 매핑합니다. RDS, Snowflake 같은 외부 서비스를 내부 서비스처럼 부를 때 유용합니다.
yamlspec:
type: ExternalName
externalName: mydb.xxxxxx.ap-northeast-2.rds.amazonaws.com
Headless Service
ClusterIP를 None으로 설정하면 가상 IP 없이 Pod IP를 직접 반환합니다. StatefulSet처럼 Pod를 개별적으로 지정해야 할 때 사용합니다.
yamlspec:
clusterIP: None
EKS에서 Service가 동작하는 흐름
클라이언트 → Service (ClusterIP) → kube-proxy (iptables/IPVS) → Pod
EKS에서 kube-proxy는 각 노드에 DaemonSet으로 실행되며, Service로 들어온 트래픽을 실제 Pod로 라우팅
AWS Load Balancer Controller란?
EKS에서 type: LoadBalancer Service를 생성하면, AWS Load Balancer Controller(이하 LBC)가 자동으로 NLB(Network Load Balancer) 를 생성하고 관리합니다.
기존 Kubernetes 기본 방식은 CLB(Classic Load Balancer)를 생성했지만, LBC를 사용하면 NLB를 생성할 수 있습니다.
LBC는 EKS 클러스터에 별도로 설치해야 합니다. IAM 권한(IRSA 또는 Pod Identity) 설정이 필수입니다.
Instance 모드 vs IP 모드
LBC에서 NLB의 타겟 등록 방식은 두 가지입니다.

❗️IP 모드는 VPC CNI 환경인 EKS에서만 사용 가능합니다.
# IP 모드 활성화
metadata:
annotations:
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
AWS Load Balancer Controller + Ingress (ALB) IP 모드 동작 with AWS VPC CNI

▫️ NLB는 L4(TCP 레벨)에서 동작하지만, TCP 80 포트로 Listen하면 HTTP 트래픽도 통과시킬 수 있습니다
하지만 어차피 HTTP도 TCP 위에서 동작하니까 NLB TCP 80으로 받아서 Pod까지 넘기면 Pod 입장에서는 HTTP 요청을 정상적으로 받습니다.
그러면 ALB 왜 쓸까? 차이는 아래와 같습니다.

Path/Host 라우팅이나 HTTPS 리다이렉트 같은 L7 기능이 필요할 때 ALB를 사용합니다.
클러스터 내부의 서비스(ClusterIP, NodePort, Loadbalancer)를 외부로 노출(HTTP/HTTPS) - Web Proxy 역할
ingress 규칙을 실제로 처리하는 구현체 (NGINX, Traefik, aws lb컨트롤러, kong 등..)
즉 Ingress는 "설계", Controller는 "실행"에 역할이 있다고 생각하면 될것같습니다.
제가 관리하는 사이트는 ingress + nginx ingress controller 조합으로 사용을 했었습니다!
NGINX Ingress Controller 를 선택했던 이유는
이 두가지가 가장 컸습니다
확실히 같은 nginx 계열이여도 NGF 보다 추가설정은 nginx ingress controller가 훨씬 쉬운 느낌이었습니다.
저희는 멀티 클러스터로 구성되어있어
아래와 유사하게 구성이 되어있습니다

ExternalDNS란?
Kubernetes에서 Service/Ingress/GatewayAPI를 생성할 때 도메인을 설정하면 Route 53(AWS), Cloud DNS(GCP), Azure DNS 등에 A 레코드를 자동으로 생성/삭제해주는 컨트롤러입니다.기존에는 ALB나 NLB를 만들고 나서 Route 53에 수동으로 레코드를 등록해야 했습니다. ExternalDNS를 사용하면 이 과정이 자동화됩니다.
동작방식
Ingress / Service 생성 (annotation에 도메인 지정)
↓
ExternalDNS가 kube-apiserver를 watch
↓
Route 53에 A 레코드 자동 생성
↓
Service / Ingress 삭제 시 레코드도 자동 삭제
EKS를 운영하고 계신다면 Ingress를 사용하시나요? 아니면 Gateway API를 사용하시나요?
저희 회사의 경우 기존에는 Ingress + NGINX Ingress Controller 조합으로 운영하고 있었습니다만 올해 3월 NGINX Ingress Controller가 EoL을 맞이하면서ㅠㅠㅎ,, Gateway API 와 NGINX Gateway Fabric 조합으로 전환을 진행 중입니다.
제가 NGF를 선택한 이유는
아직 완벽하게 전환한것은 아니지만(진행중) 사용하면서 조금 불편하다고 느꼈던 점은 이정도 였습니다.
CoreDNS란?
CoreDNS는 Kubernetes 클러스터 내부의 DNS 서버입니다.
v1.11부터 kube-dns를 대체해 공식 기본 DNS로 채택되었으며, Go로 작성된 단일 바이너리로 plugin 체인 구조 덕분에 ConfigMap 수정만으로 기능 확장이 가능합니다.
DNS 쿼리 흐름
Pod 안에서 curl my-service 를 실행하면 내부적으로 어떤 일이 벌어질까요?
Pod는 생성될 때 /etc/resolv.conf 파일을 자동으로 받습니다.
nameserver 172.20.0.10 # DNS 서버 = CoreDNS Service ClusterIP
search default.svc.cluster.local cluster.local # 자동으로 붙여볼 suffix 목록
options ndots:5 # 점이 5개 미만이면 suffix를 먼저 붙여봄
my-service 라고만 쳐도 CoreDNS가 알아서 찾아주는 이유가 여기에 있습니다.
suffix를 자동으로 붙여가며 탐색하기 때문입니다.
my-service → my-service.default.svc.cluster.local ✅ 찾음
반대로 외부 도메인 api.example.com 을 쿼리하면 문제가 생깁니다.
점이 2개뿐이라 ndots:5 조건에 걸려, 쓸모없는 쿼리를 3번 더 날린 뒤에야 실제 외부로 나갑니다.
api.example.com.default.svc.cluster.local ❌ 없음
api.example.com.svc.cluster.local ❌ 없음
api.example.com.cluster.local ❌ 없음
api.example.com ✅ 외부로 나감
외부 API를 많이 호출하는 서비스라면 이 불필요한 왕복이 레이턴시로 쌓입니다.
이럴 때 ndots:3 으로 낮추면 점이 3개 이상인 도메인은 처음부터 바로 외부로 나갑니다.
yaml# 외부 API 호출이 많은 Deployment에 선택적으로 적용
spec:
dnsConfig:
options:
- name: ndots
value: "3"
Corefile — EKS 기본 설정
EKS의 CoreDNS 설정은 kube-system 네임스페이스의 ConfigMap으로 관리됩니다.
kubectl get configmap coredns -n kube-system -o yaml
.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}

EKS에서 forward . /etc/resolv.conf 의 upstream은 VPC DNS 서버(AmazonProvidedDNS, 169.254.169.253) 입니다.
즉, 외부 도메인 쿼리는 CoreDNS → VPC DNS → Route 53 Resolver 순으로 흐릅니다.