서종호(가시다)님의 AWS EKS Workshop Study(AEWS) 2주차 학습 내용을 기반으로 합니다.
1. 기본 배포 설정 및 클러스터 네트워킹 확인
기본적으로 EKS는 클러스터 네트워킹을 위해 Amazon VPC CNI 플러그인을 사용합니다. 이 CNI의 가장 큰 특징은 Kubernetes Pod에 VPC의 기본 IP 주소 대역을 직접 할당한다는 점입니다. 따라서 Pod가 생성될 때마다 복잡한 오버레이(Overlay) 네트워크를 거치지 않고, VPC 대역의 프라이빗 IP를 하나씩 즉시 부여받게 됩니다.
Pod에 IP를 부여하기 위해 VPC CNI는 EC2 인스턴스(노드)에 다음과 같은 작업을 수행합니다.
- ENI 연결: 노드에 하나 이상의 ENI를 연결합니다. (이미지의 프라이빗 IPv4 주소에 있는 2개의 IP는 각각 연결된 ENI의 기본 IP를 의미합니다.)
- 보조 IP 할당: 각 ENI에는 EC2 인스턴스 타입에 따라 여러 개의 **보조 프라이빗 IPv4 주소(Secondary Private IP)**를 할당할 수 있습니다. (이미지의 여러 IP 목록이 이에 해당합니다.) 이 보조 IP들이 각각 개별 Pod에 1:1로 매핑됩니다.


그렇다면 왜 생성 직후부터 IP가 많이 생성되어 있는 것일까요?
이는 VPC CNI의 Warm Pool(사전 할당) 정책 때문입니다.
- 빠른 Pod 실행을 위한 캐싱: Pod가 스케줄링될 때마다 AWS API를 호출하여 IP를 새로 할당받게 되면 통신 지연 시간(Latency)이 발생하여 Pod 실행이 느려집니다.
- ipamd 데몬의 역할: 노드 내부에서 실행되는 AWS ipamd (IP Address Management daemon) 데몬은 빠른 Pod 시작을 보장하기 위해, 미리 여분의 ENI와 보조 IP들을 AWS API로부터 발급받아 노드에 '미리 준비(Warm-up)'해 둡니다.
- 기본 설정값: 기본적으로 CNI는 여분의 ENI를 하나 더 유지하려는 설정(
WARM_ENI_TARGET=1)을 가지고 있습니다. 따라서 노드가 배포되자마자 기본 ENI 외에 추가 ENI가 스스로 붙고, 해당 ENI가 가질 수 있는 최대치의 보조 IP들을 몽땅 미리 발급받아 두게 됩니다.
(실제로 위처럼 할당받은 IP들이 어떻게 사용되는지 로그에서 확인할 수 있습니다. 빨간색 박스를 친 부분은 고가용성으로 구동된 coreDNS 파드가 사용하는 IP들입니다.)

2. 노드 간 파드 통신 작동 원리
2.1. 쿠버네티스 네트워크 요구사항 3원칙
본격적인 통신 과정에 앞서 알아야 할 쿠버네티스의 공식 네트워크 모델 원칙은 다음과 같습니다.
- 모든 파드(Pod)는 NAT(Network Address Translation) 변환 없이 모든 노드의 다른 파드들과 직접 통신할 수 있어야 한다.
- 노드 상의 에이전트(예: kubelet 데몬)는 해당 노드의 모든 파드와 통신할 수 있어야 한다.
- 파드가 자신이 갖고 있다고 인식하는 IP 주소는, 다른 모든 파드가 바라보는 해당 파드의 IP 주소와 완벽히 동일해야 한다.
2.2. AWS VPC CNI 환경에서의 파드 간 통신 과정
현재 실습 중인 EKS는 AWS VPC CNI를 사용하므로, 패킷을 캡슐화하는 타 클러스터(Calico, Flannel 등) 방식과는 다르게 아래와 같은 원리로 동작합니다.
클러스터에 디플로이먼트(Deployment)로 파드를 3개 노드에 고르게 배포한 뒤, pod-2 (192.168.1.249)가 pod-1 (192.168.2.63)에게 데이터를 보내는 상황을 가정해 보겠습니다.
cat <<EOF | kubectl apply -f - 14:05:53
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: netshoot-daemonset
labels:
app: netshoot-pod
spec:
selector:
matchLabels:
app: netshoot-pod
template:
metadata:
labels:
app: netshoot-pod
spec:
containers:
- name: netshoot
image: nicolaka/netshoot
command: ["sleep", "infinity"]
EOF배포 하면 아래처럼 각 노드에 파드가 배치됩니다.

파드 통신을 위한 환경 변수를 설정해줍니다.
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 $PODIP3PODNAME1=$(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}')설정 후 아래처럼 파드 1번에 파드 2번으로의 통신이 가능합니다. 각 파드마다 테스트 해보면 됩니다.

AWS VPC CNI 환경에서의 파드 간 통신 과정 현재 실습 중인 EKS(Elastic Kubernetes Service)는 AWS VPC CNI를 사용하므로 다른 클러스터(Calico, Flannel 등 패킷을 캡슐화하는 오버레이 방식)와는 다르게 다음과 같은 원리로 동작합니다.

- Per Pod Net Namespace: 파드 내부는 외부 OS와 완전히 격리된 별도의 가상 네트워크 방(Namespace)을 가집니다. 패킷은 파드 안의 랜카드인
eth0를 거쳐 밖으로 나갑니다. - veth Pair (가상 케이블): 파드에서 나온 패킷은 노드의 진짜 네트워크 환경인 Root Net Namespace로 진입해야 합니다. 이때 두 공간을 물리적인 랜선처럼 이어주는 가상 인터페이스가 쌍으로 존재하는데, 이를 통해 트래픽이 노드의 리눅스 커널로 들어옵니다.
- 노드 OS 라우팅 및 ENI 배출: 리눅스 커널은 라우팅 테이블(
ip route)을 확인하고, 패킷을 노드 밖으로 던지기 위해 물리적인 랜카드인 ENI0 (192.168.1.64)로 내보냅니다. 이때 출발지 IP는 노드 IP로 변조(NAT)되지 않고 파드 IP 원본 그대로 유지되며 밖으로 나갑니다. - AWS VPC Router의 L3 라우팅: 워커 노드 1에서 튀어나온 패킷은 AWS VPC 내부 라우터로 향합니다. AWS 인프라는 목적지 IP가 워커 노드 2에 있다는 것을 전부 알고 있으므로 캡슐화 없이 패킷을 즉시 워커 노드 2로 건네줍니다.
- ENI 수신 및 도착지 라우팅: 패킷이 워커 노드 2의 ENI에 도착하면, 노드 2의 리눅스 커널이 파드 전용 라우팅 룰을 보고 패킷 방향을 꺾어 올립니다.
- 최종 목적지 도착: 방향을 꺾은 패킷은 워커 노드 2에 꽂혀 있는 가상 케이블(veth)을 타고 최종적으로 목적지 파드(pod-1) 내부 안으로 전송되게 됩니다.
3. EKS 노드 최대 파드 수
3.1. 파드 개수를 결정하는 요인
EKS 노드 한 개에 배치할 수 있는 파드의 최대 개수는 다음 두 가지 요소에 의해 결정됩니다.
- EC2 인스턴스 네트워크 스펙: 인스턴스에 장착할 수 있는 최대 ENI 개수와 각 ENI당 할당 가능한 IP 개수
- Kubelet의
maxPods설정: 쿠버네티스 노드 에이전트에 하드코딩된 파드 생성 제한 값
3.2. 보조 IP 모드 (Secondary IP Mode) - EKS 기본값
EKS 클러스터를 생성할 때 기본적으로 사용되는 VPC CNI 동작 방식입니다. 파드 1개당 ENI의 보조 IP 1개를 1:1로 매핑하여 할당합니다.
- 특징: 인스턴스 크기가 작을수록(예: t3.medium) 가용한 IP 수가 적어 파드를 몇 개 띄우지 못하는 IP 고갈 문제가 발생하기 쉽습니다.
최대 파드 계산 공식: (ENI 최대 개수 × (ENI당 할당 가능한 IP 개수 - 1)) + 2
-1을 하는 이유: ENI 자체의 Primary IP 1개를 제외해야 하기 때문입니다.+2를 하는 이유:aws-node(VPC CNI)와kube-proxy파드는 호스트의 네트워크(Host-networking)를 사용하여 IP를 소모하지 않기 때문입니다.
계산 예시 (t3.medium의 경우):
- 최대 ENI 개수: 3개
- ENI당 IP 개수: 6개
- 계산:
(3 × (6 - 1)) + 2 = 17개(이 중 2개는 aws-node, kube-proxy이므로 실제 추가 배포 가능한 파드는 15개)
클러스터가 사용한 EC2 타입은 t3.medium이다. 즉, 최대 17개 파드까지만 배포 가능하다. 아래처럼 명령어로도 직접 확인할 수 있다.

50개의 파드를 배포해보자. 아래처럼 8개의 파드가 Pending 상태에 걸려있다.

기본적으로 aws-node 파드 1개, kube-proxy 파드 1개는 모든 노드에 구동된다. 그리고 2가지 노드는 coreDNS 파드, 나머지 1 노드는 아래 이미지를 렌더링 해주는 kube-ops-view 파드가 구동되어서 모든 노드 당 3개씩 파드는 미리 할당된 상태이다. 즉, 각 노드당 14개씩 배포 중이라서 42개를 제외한 8개의 파드가 Pending으로 처리된다.

이 방식이 이제 보조 IP를 사용할 때의 max 파드 개수 처리 방법이다.
3.3 접두사 위임 모드 (Prefix Delegation Mode)
IP 고갈 문제를 해결하기 위해 도입된 기능입니다. 개별 IP를 하나씩 할당하는 대신, **/28 대역의 IPv4 접두사(Prefix, IP 16개 묶음)**를 ENI에 통째로 할당합니다.
- 특징: 작은 인스턴스에서도 수많은 파드에 IP를 할당할 수 있도록 IP 가용량을 비약적으로 증가시킵니다.
- 필수 조건: AWS Nitro 기반의 인스턴스(t3, c5, m5 등)에서만 동작합니다.
- 활성화 방법: VPC CNI(
aws-node데몬셋)의 환경변수ENABLE_PREFIX_DELEGATION을"true"로 설정합니다.
AWS 공식 권장 maxPods 제한: IP 할당량은 크게 늘어나지만, 무한정 파드를 생성할 수 있는 것은 아닙니다. AWS는 노드의 안정성을 위해 다음과 같은 제한을 권고(및 관리형 노드 그룹에 자동 적용)합니다.
- vCPU 30개 미만 인스턴스: 최대 110개
- vCPU 30개 이상 인스턴스: 최대 250개
3.4. 최종 최대 파드 수 선택 (우선순위)
실제 EKS 클러스터가 동작할 때 어떤 수치를 기준으로 삼을지는 다음 우선순위(높은 순서)를 따릅니다.
- 관리형 노드 그룹 자체 적용치: 가장 최우선 순위입니다. (vCPU 30 미만은 110개, 이상은 250개로 제한)
- Kubelet
maxPods설정: 사용자 지정 AMI(Custom AMI) 등을 썼을 때 수동으로 세팅한 값 - nodeadm
maxPodsExpression: NodeConfig의 표현식 결과값 - 기본 ENI 기반 계산식: 위 3개 값이 전부 없을 때 적용되는 최후의 보루.
(ENI 수 × (IP 갯수 - 1)) + 2공식을 사용합니다.
4. External DNS
K8S 서비스/인그레스 생성 시 도메인을 설정하면, AWS(Route 53), Azure(DNS), GCP(Cloud DNS) 에 A 레코드(TXT 레코드)로 자동 생성/삭제한다.

- ExternalDNS CTRL 권한 주는 방법 3가지 : Node IAM Role, Static credentials, IRSA
아래 실습은 Public 도메인 소유를 하고 있어야 테스트 가능함.
MyDomain=hongcs.gabia-cloud.net
# 자신의 Route 53 도메인 ID 조회 및 변수 지정
aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." | jq
aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Name"
aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text
MyDnzHostedZoneId=`aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text`
echo $MyDnzHostedZoneId
# (옵션) NS 레코드 타입 첫번째 조회
aws route53 list-resource-record-sets --output json --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'NS']" | jq -r '.[0].ResourceRecords[].Value'
# (옵션) A 레코드 타입 모두 조회
aws route53 list-resource-record-sets --output json --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']"
# A 레코드 타입 조회
aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq
aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A'].Name" | jq
aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A'].Name" --output text
# A 레코드 값 반복 조회
while true; do aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq ; date ; echo ; sleep 1; doneExternalDNS 설치
# EKS 배포 시 Node IAM Role 설정되어 있음
# eksctl create cluster ... --external-dns-access ...
MyDomain=hongcs.gabia-cloud.net
# 자신의 Route 53 도메인 ID 조회 및 변수 지정
MyDnzHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text)
# 변수 확인
echo $MyDomain, $MyDnzHostedZoneId
# ExternalDNS 배포
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml
cat externaldns.yaml
MyDomain=$MyDomain MyDnzHostedZoneId=$MyDnzHostedZoneId envsubst < externaldns.yaml | kubectl apply -f -
# 확인 및 로그 모니터링
kubectl get pod -l app.kubernetes.io/name=external-dns -n kube-system
kubectl logs deploy/external-dns -n kube-system -f실습 중 route53 레코드 목록을 가져오는 과정에서 아래 오류가 발생했다.
aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq INT 51s 09:36:49
aws: [ERROR]: An error occurred (NoSuchHostedZone) when calling the ListResourceRecordSets operation: No hosted zone found with ID: rrset
Additional error details:
Type: Sender직접 route53의 id 값을 찾아주고 이를 환경변수로 직접 넣어줘서 해결했다.
출력 결과에서 /hostedzone/Z0XXXXXXX...와 같이 표시된 Z로 시작하는 값(예: Z0123456789ABCDEF)이 실제 ID이다.
Service(NLB) + 도메인 연동(ExternalDNS)
테트리스 어플리케이션을 디플로이먼트로 띄우고 이를 도메인을 거쳐 서비스를 통해 외부에서 정상적으로 접근할 수 있는지 확인하는 테스트이다.
# 테트리스 디플로이먼트 배포
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: tetris
labels:
app: tetris
spec:
replicas: 1
selector:
matchLabels:
app: tetris
template:
metadata:
labels:
app: tetris
spec:
containers:
- name: tetris
image: bsord/tetris
---
apiVersion: v1
kind: Service
metadata:
name: tetris
annotations:
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "http"
#service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "80"
spec:
selector:
app: tetris
ports:
- port: 80
protocol: TCP
targetPort: 80
type: LoadBalancer
loadBalancerClass: service.k8s.aws/nlb
EOF
# 배포 확인
kubectl get deploy,svc,ep tetris
# NLB에 ExternanDNS 로 도메인 연결
kubectl annotate service tetris "external-dns.alpha.kubernetes.io/hostname=tetris.$MyDomain"
while true; do aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq ; date ; echo ; sleep 1; done
# Route53에 A레코드 확인
aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq
# 확인
dig +short tetris.$MyDomain @8.8.8.8
dig +short tetris.$MyDomain
# 도메인 체크
echo -e "My Domain Checker Site1 = https://www.whatsmydns.net/#A/tetris.$MyDomain"
echo -e "My Domain Checker Site2 = https://dnschecker.org/#A/tetris.$MyDomain"
# 웹 접속 주소 확인 및 접속
echo -e "Tetris Game URL = http://tetris.$MyDomain"위와 같이 테트리스 배포한 다음 http://tetris.hongcs.gabia-cloud.net/ 에 접속을 해보면 실제 테트리스 게임을 수행해볼 수 있다. (지금은 구동 중료 해두었습니다)
참고로 실습 후, 서비스 및 디플로이먼트를 제거하면 route53의 리소스가 함께 자동으로 함께 삭제되는데 이는 아마 --policy=sync 로 값이 설정되어서 자동으로 레코드도 삭제되는것으로 확인했다.
5. Core DNS
CoreDNS는 EKS와 관련 없이 기본적으로 k8s에서 구동되는 파드임을 이미 우리는 확인했다. (2개 파드 정도가 구동되면서 고가용성 보장)
아래는 대략적으로 파드 foo에서 dns 리졸빙을 하는 과정을 나타낸 다이어그램이다.

이를 크게 두가지 방식으로 나눠서 정리할 수 있다. EKS 환경에서의 동작을 기준으로 정리했다.
1. 파드에서 외부 도메인(hongchangsub.com) 조회 시 내부 동작
파드에서 클러스터 외부의 도메인을 찾을 때, 쿼리는 클러스터를 빠져나가 AWS VPC의 기본 DNS를 거쳐 퍼블릭 인터넷으로 향하게 된다.
- 파드 내부 쿼리 발생: 파드 내부의 애플리케이션이
hongchangsub.com에 대한 룩업을 요청한다. 파드의/etc/resolv.conf에는 nameserver가 NodeLocal DNSCache의 IP(보통 169.254.20.10)로 설정되어 있다. - NodeLocal DNSCache: 쿼리가 NodeLocal DNSCache로 전달된다. 캐시에 해당 도메인이 없다면(Cache Miss), 쿼리를 CoreDNS로 포워딩한다
- CoreDNS 포워딩: CoreDNS는 쿼리를 받아 분석합니다.
hongchangsub.com은 자신이 관리하는 내부 도메인(cluster.local)이 아니므로, CoreDNS의Corefile설정(forward 플러그인)에 따라 상위 DNS로 쿼리를 넘긴다. - AWS VPC DNS (AmazonProvidedDNS): EKS 환경에서 CoreDNS의 상위 DNS는 워커 노드가 속한 VPC의 기본 DNS이다. 이는 일반적으로 VPC IPv4 네트워크 대역의 기본 주소에 2를 더한 IP(예: VPC가
10.0.0.0/16이면10.0.0.2)이다. - Route 53 Resolver: VPC DNS는 내부적으로 AWS Route 53 Resolver를 사용하여 퍼블릭 DNS 계층(Root -> TLD -> Authoritative Nameserver)을 거쳐
hongchangsub.com의 실제 IP 주소를 알아낸다. - 응답 반환: 찾은 IP 주소가 다시 AWS VPC DNS -> CoreDNS -> NodeLocal DNSCache -> 파드 순서로 반환된다. (이 과정에서 캐시가 업데이트됩니다.)
2. 파드에서 내부 클러스터 도메인 조회 시 내부 동작
파드에서 같은 클러스터 내부의 서비스(예: my-service.default.svc.cluster.local)를 찾을 때는 쿼리가 클러스터 외부로 나가지 않고 CoreDNS 선에서 처리된다.
- 파드 내부 쿼리 발생: 파드에서 내부 도메인 룩업을 요청 후에 마찬가지로 쿼리는 NodeLocal DNSCache로 향한다.
- NodeLocal DNSCache: 캐시에 결과가 없다면 CoreDNS로 쿼리를 포워딩된다.
- CoreDNS 조회: CoreDNS는
cluster.local도메인에 대한 권한을 가진 네임서버이다. CoreDNS는 쿠버네티스 API 서버와 지속적으로 통신하며 Service와 Endpoint 정보를 메모리에 가진다. - 결과 확인 및 응답: CoreDNS는 즉시 자신이 알고 있는
my-service의 ClusterIP(또는 Headless Service의 경우 파드 IP 목록)를 찾아 응답한다. - 응답 반환: CoreDNS -> NodeLocal DNSCache -> 파드 순서로 IP 주소가 반환된다.
처음 공부할 때는 coreDNS와 externalDNS가 관련이 있는 기능들로 오해했다. 하지만 실제로 이 둘은 목적이 전혀 다른 기능들이다. externalDNS는 결국 EKS를 사용한다면 route53과 관련되어 서비스 또는 인그레스 생성 시 해당 도메인에 대한 레코드를 실제로 생성 및 관리하는 역할을 수행해주는 기능을 수행한다.