2W - EKS VPC CNI 분석
개요
본 글은 다음의 내용을 설명하고, 이를 위한 실습을 진행한다.
- VPC CNI의 특징, 세팅
- IP 추가 할당 방법
- 트래픽 분석
이 노트에서는 EKS의 가장 기본이 되는 네트워크 환경과 리소스를 분석한다.
사전 지식
VPC CNI란
AWS 클라우드 환경에 최적화되어 있는 네트워크 플러그인인데, 아주 큰 특징이 있다.
바로 VPC의 IP 대역을 파드에게 할당한다는 것..
얼핏 들으면 IP 대역을 빠르게 소모시키는 IP 먹는 하마 같지만(하마는 맞는 것 같다), 그만큼 가지는 이점이 있다.
IP를 할당하는 방식은 인스턴스 ENI의 보조 IP를 주는 방식이다.
그런데 ENI는 스펙마다 최대로 받을 수 있는 IP 개수가 정해져 있다.
그래서 이를 초과하는 만큼 파드가 생성되는 경우에는 추가적인 ENI가 붙게 된다.
그런데 인스턴스는 스펙마다 최대로 받을 수 있는 ENI 개수도 정해져 있다!
그래서 사실.. 한 인스턴스에는 최대로 생성할 수 있는 파드의 개수가 제한된다..
물론 이걸 설정하는 방법이 존재하는데, 아래에서 보자.
장점
- 트래픽 최적화
- 기본적인 CNI들은 VXLAN, IPIP 등의 방법으로 오버레이 네트워크를 구성하여 클러스터 파드 IP 대역을 만든다.
- 이러한 방식은 패킷을 한번 더 캡슐화시키거나, 홉을 증가시킴으로써 비효율을 야기한다.
- 그러나 네트워크 인터페이스에서 트래픽을 받아 바로 파드의 프로세스로 보내면 엄청난 속도 향상을 꾀할 수 있다.
- 쉬운 모니터링
- AWS 리소스로 파드를 쉽게 추적할 수 있다.
- 다른 리소스와의 연계
- VPC IP를 받기 때문에 다양한 추가 기능을 활용할 수 있게 된다.
- 대표적인 것이 로드밸런서 IP모드이다.
구조 및 동작과정
구체적으로 VPC CNI라고 하면 두 가지 구성 요소가 포함된다.
kube-system
네임스페이스에 데몬셋으로 확인할 수 있는데, 그래서 이 친구는 컨테이너도 두 개다.
- CNI 바이너리
- 기능에 충실한 CNI로서, 파드 간의 통신을 담당하는 역할을 수행한다.
- IPAMD
- 노드의 ENI를 관리하며, 사용 가능한 ip 주소 풀을 유지한다.
- 이때 관리하는 풀을 WARM POOL이라고 부르며, 할당할 수 있는 IP 주소를 미리 확보해둠으로써 파드가 띄워질 때 빠르게 IP를 할당할 수 있도록 돕는다.
두 요소는 서로 gRPC로 통신하며 각종 작업을 수행하게 된다.[1]
CNI 바이너리가 실제로 IP를 할당하고, 그 IP 자체는 IPAM에서 받아온다고 이해하면 딱 맞겠다.
IPAM은 어떻게 IP를 확보할까?
위에서 대충 말했지만, 일단 IP를 받을 공간이 있는지 보고, 없으면 ENI를 하나 더 받아오는 식이다.
최대 IP 개수
aws ec2 describe-instance-types \
--filters "Name=instance-type,Values=c5.*" \
--query "InstanceTypes[].{ \
Type: InstanceType, \
MaxENI: NetworkInfo.MaximumNetworkInterfaces, \
IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
--output table
이런 식으로 인스턴스 스펙 당 최대로 붙일 수 있는 ENI개수와, 그 ENI가 받을 수 있는 최대 IP 개수를 볼 수 있다.
그럼 내가 사용할 수 있는 ip는 총 몇 개일까?
맨 위 c5.4xlarge를 예로 들어보겠다.
일단 인스턴스에 ENI가 부착되면 한 IP는 노드 자체에 대해 할당된다.
그렇기에 각 ENI당 파드에 할당할 수 있는 IP 주소 개수는 30 - 1 인 29개이다.
그렇기에 해당 인스턴스에 배치할 수 있는 최대 파드는 29 * 8 인 232개가 된다.
클러스터 운영을 위해 기본으로 돌아가는, kube-proxy와 cni 파드가 hostNetwork로 돌아가고 있기에 한 노드당 최대 파드를 구하려면 사실 마지막에 2를 더해주어야 하는데, 어차피 이건 우리가 운영에 직접 사용할 파드도 아니라 제외한다.
kubectl describe node | grep Allocatable: -A6
클러스터의 정보로서도 파드의 개수가 표시되므로, 이렇게도 볼 수 있다.
현재 내 인스턴스는 t3.medium으로, 호스트 네트워크 파드까지 포함하여 17개까지 파드를 둘 수 있다고 나온다.
설정 방법
이건 콘솔에서 커스텀 세팅을 한 모습이다.
기본적으로 설정 파일에 대한 스키마를 보고 그것에 맞게 넣어주면 된다.
aws eks describe-addon-configuration --addon-name vpc-cni --addon-version v1.15.1-eksbuild.1 | jq '.configurationSchema | fromjson'
이런 식으로 스키마를 볼 수 있고, 콘솔에서도 찾아볼 수 있다.
IP 개수 늘리기
VPC CNI의 단점은 IP가 빠르게 소진된다는 것이다.
이를 해결하기 위한 여러가지 방법이 있다.
custom networking
먼저 이 방식은 인스턴스에 배치할 수 있는 파드 개수를 늘리는 것은 아니고, vpc내의 ip 대역을 늘리는 설정이라고 봐야한다.
이런 식으로, VPC 내에 완전히 새로운 서브넷을 지정해서 이 IP를 할당해주는 방법이 존재한다![2]
이걸 활용하는 게 바로 커스텀 네트워킹으로, 새로운 IP 대역을 받아 할당할 수 있게 된다.
10.42 대역의 VPC에서 100.64 대역을 배치하는 괴상한 방식이 가능한 것이다!
prefix delegation
작은 일만 하는 파드들인데 이 제한으로 인해 노드를 여러 개 쓰는 것은 큰 비효율이다.
그래서 한 인스턴스에서 실행할 파드를 늘리기 위해 AWS에서는 접두사 위임(prefix delegation)기능을 제공한다[3]
기존에 각 ENI에서 하나의 파드에 하나씩 할당했던 Secondary IP들이, 전부 하나의 작은 서브넷처럼 작동한다.
원래 그냥 ip를 주던 것으로 끝내지 않고 활용할 수 있는 서브넷 대역을, 즉 접두사를 활용할 수 있게 준다고 하여 접두사 위임이라 부른다.
이렇게 하면 ..../28의 서브넷을 내부적으로 위임받아 파드에 할당할 수 있게 된다.
인스턴스가 받을 수 있는 각 IP들이 내부의 작은 서브넷이 되어 각각 16개의 추가 IP를 할당할 수 있도록 해주는 것이다!
그냥 보면 헷갈릴 수 있겠지만, 각 ip 대역은 4번째 옥텟의 상위 4비트가 전부 달라서 문제가 발생하지 않는다.
위 사진에서도 보이듯이, 이게 설정되면 각 노드는 미리 접두사를 할당 받게 된다.
참고로 이렇게 한 노드에 배치할 수 있는 파드의 개수를 늘릴 수는 있지만, 이것과 별개로 kubelet 단에서 제한하는 최대 파드 개수가 있다.
그렇기 때문에 이 기능을 적용하려면 최소한 kubelet을 재기동시켜줘야 한다.
또한 NItro 하이퍼바이저를 사용한 인스턴스에 대해서만 이 기능을 사용할 수 있다.
실습 진행
환경 세팅에 대한 부분은 2W - 테라폼으로 환경 구성 및 VPC 연결에 담겨져있다!
대충 모양은 이렇게 구성돼있어서 운영 환경의 VPC와 EKS VPC를 분리한 상태이다.
간단하게 VPC CNI 로그 확인
tail -f /var/log/aws-routed-eni/ipamd.log
각 노드로 들어가면 주소를 어떻게 관리하고 있는지 로그를 확인할 수 있다.
일단 현재 붙은 인터페이스를 추적하고 여기에 미리 선제적으로 ip들을 받아둔다.
그리고 파드가 생성될 때, 풀에 확보된 ip를 전달해준다.
custom networking 세팅
먼저 VPC에 추가적인 IP cidr을 설정하는 것으로 시작한다.
VPC cidr 대역을 추가 할당했다면 거기에 상응하는 서브넷도 만들어주면 기본 준비는 끝났고, 이제 CNI 세팅을 건드리자.
kubectl set env daemonset aws-node -n kube-system AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG=true
vpc cni 쪽에서는 데몬셋 환경 설정을 통해 활성화할 수 있다.
환경 변수의 변경도 컨테이너의 변경으로 치기 때문에 알아서 워크로드는 재시작된다.
apiVersion: crd.k8s.amazonaws.com/v1alpha1
kind: ENIConfig
metadata:
# 가용영역 이름을 웬만해서 넣어야 한다.나도 알고 싶지 않았다.
name: ${SUBNET_AZ}
spec:
# 허용하고자 하는 보안그룹 id
securityGroups:
- ${EKS_CLUSTER_SECURITY_GROUP_ID}
# 만들어둔 서브넷 id
subnet: ${SECONDARY_SUBNET}
여기에 ENIConfig
라는 CRD를 만들어주면 진짜 준비는 끝이다.
어떤 AZ에 어떤 추가 서브넷이 있는지 명시하는 방식이다.
특이한 점은 가용영역을 이름으로 넣는다는 것.
만약 하나의 가용영역에 여러 개의 서브넷이 있는 상황이라면 당연히 이름 충돌이 발생한다.
이 경우에는, 이름은 알아서 만든 후에 각 노드 별로 k8s.amazonaws.com/eniConfig=EniConfigName
이런 식으로 어노테이션을 붙여줘야 한다.[4]
테스트
나는 직접적으로 하나를 넣어보았다.
kubectl set env daemonset aws-node -n kube-system ENI_CONFIG_LABEL_DEF=topology.kubernetes.io/zone
이제 CNI에서 해당 CONFIG 파일을 인식할 수 있도록 재동작을 시켜주면 된다.
이 다음에는? 해당 서브넷을 쓰는 새로운 노드 그룹을 만들면 된다!
간단하게 콘솔로만 진행했다.
파드를 해당 노드에 투입해봤다.
VPC의 IP가 부족하다면 이런 식으로 추가 대역을 넣어서 올리는 방법이 유용할 것이다.
다만, 이 경우 추가적인 노드를 추가시켜야 한다는 것이 마음에 걸린다.
prefix delegation
kubectl set env daemonset aws-node -n kube-system ENABLE_PREFIX_DELEGATION=true
이것도 설정 자체는 이렇게 CNI에 환경 설정을 넣는 것만으로 가능하다.
콘솔로 확인해보면 기존에 붙어있던 eni에 prefix delegation이 붙는 걸 볼 수 있다.
노드에 기본적으로 세팅할 수 있는 파드 개수의 최대값이 이미 지정되어 있기에 인스턴스를 재기동해야 할 수도 있다.
파드 최대 개수 설정이 재설정이 돼야 하기 때문이다.
이때 커스텀 런치 템플릿을 사용한 경우, 거기에서 파드 최대 개수를 명시적으로 바꿔줘야 한다.
런치 템플릿이 따로 없던 노드 그룹만 할당 파드 개수가 제대로 적용됐다.
이제 한 노드에 파드 폭격을 가해도 제대로 파드들이 여러 개 만들어진다!
통신 경로 분석
이제 VPC CNI로 구성된 클러스터의 네트워크 경로를 간단하게 분석해보고자 한다.
대충 예시 파드를 하나 만들어보고 확인해보자.
여기에서 192.168.1.112 파드는 어떻게 통신이 될 수 있을까?
해당 노드에 들어가서 ip addr
, ip route
를 했을 때의 값이다.
112 ip로 가는 통신은 eni77~로 가도록 되어 있다.
그럼 이 인터페이스는 또 어디로 이어지는지 확인해보자.
lsns -t net
lsns를 통해 현 호스트이 네임스페이스를 확인할 수 있다.
이때 type을 지정해서 네트워크 네임스페이스를 확인해보면..
현재 띄워져있는 파드의 pause container 프로세스가 보인다!
export PID=$(lsns -t net | grep pause | awk '{print $4}')
nsenter -t $PID --net ip addr
nsenter -t $PID --net ip route
프로세스 id까지 알았으니, 이제 현 터미널을 해당 네임스페이스를 사용하게 만들 수 있다.
ip link
nsenter -t $PID --net ip link
그럼 이제 어떤 인터페이스가 서로 연결됐는지도 확인할 수 있다.
루트 네임스페이스에서 eni22bc~는 link-netns로 cni-23~로 연결된다는 것을 알 수 있으며, 파드의 네임스페이스에선 id 0으로 연결이 되는 것이 확인된다.
ip netns list
이렇게 ip 명령어를 통해 해서 보는 방법이 있다(물론 위의 lsns로도 확인 가능하다).
이렇게 컨테이너로 어떻게 통신이 이뤄지는지 비로소 알 수 있다.
실제 통신 확인
이번에는 실제로 패킷이 어떻게 전달되는지 직접적으로 확인해보자.
apiVersion: v1
kind: Pod
metadata:
name: test-pod
labels:
app: target
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
nodeName: ip-192-168-1-61.ap-northeast-2.compute.internal
---
apiVersion: v1
kind: Pod
metadata:
name: inside-node
spec:
containers:
- image: nicolaka/netshoot
name: test
command:
- sh
- -c
- "sleep infinity"
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- target
topologyKey: "kubernetes.io/hostname"
---
apiVersion: v1
kind: Pod
metadata:
name: inter-node
spec:
containers:
- image: nicolaka/netshoot
name: test
command:
- sh
- -c
- "sleep infinity"
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- target
topologyKey: "kubernetes.io/hostname"
테스트용 nginx를 띄운 뒤, Affinity 세팅을 통해 한 파드는 같은 노드에, 다른 하나는 다른 노드에 띄워지도록 만들었다.
각각의 파드의 ip는 이렇게 할당됐다.
k exec -ti inside-node -- zsh
이런 식으로 각 파드에 들어가서 통신을 해볼 것이다.
tcpdump -nn -i any dst port 80
이런 식으로 패킷을 추적한다.
노드 내부 통신
테스트 파드가 있는 노드에서 먼저 추적해본다.
tcpdump -nn -i any dst 192.168.1.96
test-pod는 96으로 끝나는 ip를 가지고 있으니 파드로 가는 요청을 한번 보자.
97로 끝나는 inside-node 파드에서 요청이 날아갔다.
참고로 eni 인터페이스는 inside-node, test-pod에게 붙은 네트워크 인터페이스이다.
서로가 서로에게 바로바로 통신을 주고받는 것이 확인된다.
사진으로 싣지는 않겠지만, inside node의 파드의 eth0에서 트래픽이 나갈 때는 97로 나가는데, 이후 노드의 라우팅 테이블에서 96이라는 ip에 대해 eni7fba~로 가도록 되어 있었기 때문에 자연스럽게 별다른 트래픽 이동없이 바로 연결이 된 것이다.
노드 간 통신
k exec -ti inter-node -- zsh
이번에는 다른 노드에서 요청을 보낼 것이다.
이번에도 192.1682.96, 즉 해당 파드의 ip를 달고 통신이 이뤄진 것을 확인할 수 있다.
그러나 이것은 얼핏 보면 조금 이상하다.
노드를 나가는 트래픽이 다시 돌아오기 위해서는 통상적으로 노드의 IP를 소스로 가지는 식으로, 즉 SNAT를 해서 나가야만 제대로 돌아올 수 있다.
그래서 흔히 노드의 ip가 추적됐어야 할 것만 같다.
그런데 위의 결과는 소스가 파드의 ip를 그대로 가지고 있는 모습이다.
iptables --list -t nat -n | grep AWS -A1 -B3
해당 답의 힌트는 iptables에서 얻을 수 있다.
트래픽이 나가기 직전에 적용되는 POSTROUTING 체인을 보면 쿠버 클러스터에 흔히 세팅되는 KUBE-POSTROUTING외에 AWS-SNAT-CHAIN-0을 볼 수 있다.
그리고 해당 체인은 192.168.0.0/16
에 대해서 RETURN을 하므로, 그대로 POSTROUTING으로 돌아가게 되고 결국 SNAT 없이 트래픽은 인터페이스를 타고 나가게 된다.
ip route get 192.168.1.96
참고로 해당 경로를 나갈 때는..
이렇게 ens5라는, 인스턴스 바깥으로 나가는 경로를 탄다.
SRC 힌트가 붙어 있어서 무조건 SNAT이 될 거라고 생각했지만, 막상 규칙으로는 SNAT 없이 게이트웨이로 가게 되는 것이다.
그래서 결과적으로 VPC CNI는 클러스터 내부에서 모두 자신의 IP를 달고 통신을 하게 된다.
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'
참고로 이런 식으로 watch를 해두면 패킷이 나가는 개수도 확인할 수 있다.
노드 외부로 통신
ping google.com
마지막으로 외부로 통신이 나가는 상황을 본다.
테스트는 test-pod가 있는 위치에서 진행했다.
tcpdump -nn -i any icmp
이번에는 파드에서 나가는 요청이 SNAT되어 ens5 인터페이스를 탈 때는 노드의 IP가 찍히는 것을 확인할 수 있다.
위의 iptables(위 그림은 다른 노드에서 찍은 거지만 아무튼..)에서 RETURN되지 않고 SNAT 규칙에 걸렸기 때문이다.
SNAT 비활성화
당연히 SNAT되는 게 정상적인 것 같지만, 이를 비활성화하는 것도 가능하다.
만약 VPC 피어링을 하고 있는 경우라면 어차피 서로의 대역에 대해 경로를 알고 있으므로 비활성되더라도 통신에 문제는 없을 것이다.
먼저 확인을 위해 운영 호스트 보안 그룹에 eks 클러스터 보안그룹에서의 ping을 허용하도록 세팅했다.
k exec -ti inside-node -- ping 172.20.0.25
파드에서 운영 호스트로 ping을 날리니 워커 노드의 ip로 값이 들어오는 것이 확인된다.
kubectl set env daemonset aws-node -n kube-system AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS=172.20.0.0/16
CNI에 이렇게 환경 변수를 설정해주면, SNAT를 하지 않을 대역을 지정할 수 있다.
다시 ping을 날리면, 이번에는 파드의 IP가 그대로 찍히는 것이 확인된다!
참고로 1.8 버전 이전의 VPC CNI는 secondary 네트워크 인터페이스에서 ip를 할당 받은 파드의 경우에 대해 들어오고 나가는 인터페이스 경로가 달라서 rp_filter에 의해 패킷이 드랍되는 경우가 있었다고 한다.[5]
결론
VPC CNI는 파드가 노드와 같은 IP를 가지게 하는 특이한 세팅을 통해, 추가적인 패킷 캡슐화 없이 통신이 가능하게 해준다.
그만큼 빠르게 IP가 소모될 여지가 있지만 이를 다양한 세팅을 통해 해결할 수 있다.
다음 글에서 다룰 텐데, 이렇게 VPC의 IP를 받음으로써 다른 네트워크 리소스들에서 할 수 있는 기능들이 생기기 때문에 웬만하면 EKS를 쓸 때는 VPC CNI를 쓰는 것이 강권된다.
이전 글, 다음 글
다른 글 보기
이름 | index | noteType | created |
---|---|---|---|
1W - EKS 설치 및 액세스 엔드포인트 변경 실습 | 1 | published | 2025-02-03 |
2W - 테라폼으로 환경 구성 및 VPC 연결 | 2 | published | 2025-02-11 |
2W - EKS VPC CNI 분석 | 3 | published | 2025-02-11 |
2W - ALB Controller, External DNS | 4 | published | 2025-02-15 |
3W - kubestr과 EBS CSI 드라이버 | 5 | published | 2025-02-21 |
3W - EFS 드라이버, 인스턴스 스토어 활용 | 6 | published | 2025-02-22 |
4W - 번외 AL2023 노드 초기화 커스텀 | 7 | published | 2025-02-25 |
4W - EKS 모니터링과 관측 가능성 | 8 | published | 2025-02-28 |
4W - 프로메테우스 스택을 통한 EKS 모니터링 | 9 | published | 2025-02-28 |
5W - HPA, KEDA를 활용한 파드 오토스케일링 | 10 | published | 2025-03-07 |
5W - Karpenter를 활용한 클러스터 오토스케일링 | 11 | published | 2025-03-07 |
6W - PKI 구조, CSR 리소스를 통한 api 서버 조회 | 12 | published | 2025-03-15 |
6W - api 구조와 보안 1 - 인증 | 13 | published | 2025-03-15 |
6W - api 보안 2 - 인가, 어드미션 제어 | 14 | published | 2025-03-16 |
6W - EKS 파드에서 AWS 리소스 접근 제어 | 15 | published | 2025-03-16 |
6W - EKS api 서버 접근 보안 | 16 | published | 2025-03-16 |
7W - 쿠버네티스의 스케줄링, 커스텀 스케줄러 설정 | 17 | published | 2025-03-22 |
7W - EKS Fargate | 18 | published | 2025-03-22 |
7W - EKS Automode | 19 | published | 2025-03-22 |
8W - 아르고 워크플로우 | 20 | published | 2025-03-30 |
8W - 아르고 롤아웃 | 21 | published | 2025-03-30 |
8W - 아르고 CD | 22 | published | 2025-03-30 |
8W - CICD | 23 | published | 2025-03-30 |
9W - EKS 업그레이드 | 24 | published | 2025-04-02 |
10W - Vault를 활용한 CICD 보안 | 25 | published | 2025-04-16 |
11W - EKS에서 FSx, Inferentia 활용하기 | 26 | published | 2025-04-18 |
11주차 - EKS에서 FSx, Inferentia 활용하기 | 26 | published | 2025-05-11 |
12W - VPC Lattice 기반 gateway api | 27 | published | 2025-04-27 |
관련 문서
이름 | noteType | created |
---|---|---|
VPC CNI | knowledge | 2025-02-11 |