새소식

Kubernetes

[Study 9주차] EKS Network - VPC CNI

  • -

CoudNet@ 팀의 가시다님께서 리딩하시는 KANS Study (Kubernetes Advanced Networking Study) 9주차 스터디 내용 정리

 

이번 주차는 스터디의 마지막 주차로 EKS Network 에 대해 깊게 알아봤습니다.



1. 실습 환경 구성

 

실습환경은 다음과 같이 구성했습니다.

 

출처: kans study

 

 

CloudFormation 을 통해서 EKS 환경을 구성했습니다.
EKS 구성에는 약 20분 정도 소요됩니다.

 

# CloudFormation 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/kans/eks-oneclick.yaml

# 실행
STACK_NAME=kimalarm-stack
AWS_REGION=ap-northeast-2
KEY_NAME=kimalarmkey

aws cloudformation deploy \
  --template-file eks-oneclick.yaml \
  --stack-name ${STACK_NAME} \
  --parameter-overrides KeyName=${KEY_NAME} SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32  MyIamUserAccessKeyID=<AccessKey> MyIamUserSecretAccessKey='<Secret Key>' ClusterBaseName=myeks \
  --region ${AWS_REGION}

#접속
ssh -i ~/.ssh/kimalarmkey.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)

 

구성 후 다음과 같이 환경을 확인해줍니다.

# NS 변경
kubectl ns default

# 노드 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
N1=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2a -o jsonpath={.items[0].status.addresses[0].address})
N2=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2b -o jsonpath={.items[0].status.addresses[0].address})
N3=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2c -o jsonpath={.items[0].status.addresses[0].address})
echo "export N1=$N1" >> /etc/profile
echo "export N2=$N2" >> /etc/profile
echo "export N3=$N3" >> /etc/profile
echo $N1, $N2, $N3

# 노드 보안그룹 ID 확인 및 변수 등록
aws ec2 describe-security-groups --filters Name=group-name,Values=*ng1* --query "SecurityGroups[*].[GroupId]" --output text
NGSGID=$(aws ec2 describe-security-groups --filters Name=group-name,Values=*ng1* --query "SecurityGroups[*].[GroupId]" --output text)
echo $NGSGID
echo "export NGSGID=$NGSGID" >> /etc/profile

# 노드 보안그룹에 eksctl-host 에서 노드(파드)에 접속 가능하게 룰(Rule) 추가 설정
aws ec2 authorize-security-group-ingress --group-id $NGSGID --protocol '-1' --cidr 192.168.1.100/32

 

EKS 가 배포되었다면, 추가 기능에 다음과 같이 Kube-proxy, CoreDNS, VPC-CNI 가 설치된 것을 확인할 수 있습니다.



2. VPC CNI

 

2.1. 개요

 

VPC CNI 는 AWS VPC 에서 동작하는 EKS 를 위해 만들어진 K8S CNI 입니다.
VPC CNI 의 가장 큰 특징은 Kubernetes Pod 가 노드와 동일한 네트워크 대역을 사용한다는 것입니다.
그렇기 때문에, Pod 내의 모든 컨테이너는 네트워크 네임스페이스를 공유하며, 서로 간에 로컬 포트를 통해 통신할 수 있습니다.

 

Amazon VPC CNI 두 가지 구성 요소

 

1. CNI 바이너리

- Pod 간 통신을 설정하는 Pod 네트워크를 설정
- 노드의 루트 파일 시스템에서 실행
- kubelet 이 새 Pod 를 노드에 추가하거나 기존 Pod 을 삭제할 때 호출

 

2. ipamd
- 장기간 실행되는 노드 로컬 IP 주소 관리(IPAM) 데몬
- 노드에서 ENI 관리

 

노드가 프로비저닝되면 CNI 플러그인은 자동으로 노드의 서브넷에서 기본 ENI에 사용할 슬롯(IP 또는 Prefix)의 풀을 할당합니다.
이를 Warm Pool 이라고 하며, 노드의 인스턴스 유형에 따라 결정됩니다.
또한 Secondary ENI 에 별도의 슬롯 풀을 할당할 수 있는데 이 또한 인스턴스 유형에 따라 개수가 제한됩니다.

 

출처: https://aws.github.io/aws-eks-best-practices/networking/vpc-cni/

 

Pod 는 Warm Pool 의 IP 주소를 사용하기 때문에, EC2 인스턴스에서 실행할 수 있는 Pod 수는 해당 인스턴스에 연결할 수 있는 ENI 수와 각 ENI가 지원하는 슬롯 (IP) 수에 따라 결정됩니다.

이러한 제약을 해결하기 위해 VPC CNI 에는 Prefix DelegationCustom Networking 이 추가되었습니다.

 

1. IPv4 Prefix Delegation

- IPv4 28bit 서브넷(prefix)를 위임하여 할당 가능 IP 수와 인스턴스 유형에 권장하는 최대 갯수로 선정

 

2. AWS VPC CNI Custom Networking

- 노드와 파드 대역 분리, 파드에 별도 서브넷 부여 후 사용



2.2. Calico CNI vs VPC CNI

 

앞서 언급했던 것처럼 VPC CNI 의 가장 큰 차이점은 노드와 파드의 네트워크 대역이 동일하다는 것입니다.

 

출처: kans study

 

또한, 아래 그림과 같이 파드간 통신 시 일반적인 K8S CNI 는 오버레이(VXLAN, IP-IP 등) 통신을 하고, AWS VPC CNI 는 동일 대역으로 직접 통신을 합니다.

 

출처: kans study

 

 

2.3. VPC CNI 기본 정보 확인

 

VPC CNI 는 데몬셋으로 동작합니다.

# DamonSet 확인
kubectl describe daemonset aws-node --namespace kube-system | grep Image | cut -d "/" -f 2

# 노드 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

 

 

노드 네트워크 확인

 

EKS 노드의 기본 구성은 아래 그림과 같습니다.

출처: kans study

 

t3.medium 의 경우 ENI 마다 최대 6개의 IP 를 가집니다.
AWS 콘솔에서 노드 네트워크를 확인하면 보조 IP 를 가지고 있는 것을 알 수 있습니다.

 

 

노드의 보조 IP 를 파드가 사용하는 지 아래 명령어로 확인해봅니다.

 

# 신규 터미널 1 -> 노드 1번 접속
ssh ec2-user@$N1
watch -d "ip link | egrep 'eth|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"

# 신규 터미널 2 -> 노드 2번 접속
ssh ec2-user@$N2
watch -d "ip link | egrep 'eth|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"

# 신규 터미널 3 -> 노드 3번 접속
ssh ec2-user@$N3
watch -d "ip link | egrep 'eth|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"

#####

# 테스트 파드 생성
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: nicolaka/netshoot
        command: ["tail"]
        args: ["-f", "/dev/null"]
      terminationGracePeriodSeconds: 0
EOF

 



2.4. 노드 간 파드 통신

 

AWS VPC CNI 경우 별도의 오버레이(Overlay) 통신 기술 없이, VPC Native 하게 파드간 직접 통신이 가능합니다.

출처: kans study

 

# 파드 이름 변수 지정
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})

# 파드 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})

# 파드1 Shell 에서 파드2로 ping 테스트
kubectl exec -it $PODNAME1 -- ping -c 2 $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

 

 

tcpdump 확인

 

# 각 파드에 tcpdump 실행
sudo tcpdump -i any -nn icmp

 

 

노드 IP 가 아닌 Pod IP 가 바로 통신하는 것을 알 수 있습니다.



2.5. 파드에서 외부 통신

 

iptable 에 SNAT 을 통하여 노드의 eth0 IP로 변경되어서 외부와 통신됩니다.

 

출처: https://github.com/aws/amazon-vpc-cni-k8s/blob/master/docs/cni-proposal.md

 

# 작업용 EC2 : 퍼블릭IP 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s ipinfo.io/ip; echo; echo; done

# Pod 에서 외부 통신 IP 확인
for i in $PODNAME1 $PODNAME2 $PODNAME3; do echo ">> Pod : $i <<"; kubectl exec -it $i -- curl -s ipinfo.io/ip; echo; echo; done

 



2.6. 노드 파드 생성 개수 제한

 

Pod 생성 개수 제한 확인을 위해 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=LoadBalancer --set env.TZ="Asia/Seoul" --namespace kube-system

# kube-ops-view 접속 URL 확인 (1.5 배율)
kubectl get svc -n kube-system kube-ops-view -o jsonpath={.status.loadBalancer.ingress[0].hostname} | awk '{ print "KUBE-OPS-VIEW URL = http://"$1":8080/#scale=1.5"}'

 

노드의 파드 개수 계산 공식

 

최대 파드 생성 갯수 : (인스턴스 ENI 개수 × (ENI 의 IP 개수 - 1)) + 2

 

# t3 타입의 정보(필터) 확인
aws ec2 describe-instance-types --filters Name=instance-type,Values=t3.* \
 --query "InstanceTypes[].{Type: InstanceType, MaxENI: NetworkInfo.MaximumNetworkInterfaces, IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
 --output table

 # 워커노드 상세 정보 확인 : 노드 상세 정보의 Allocatable 에 pods 에 17개 정보 확인
kubectl describe node | grep Allocatable: -A6

 

 

이제 Pod 개수를 최대치로 배포해보겠습니다.

 

# 워커 노드 EC2 - 모니터링
while true; do ip -br -c addr show && echo "--------------" ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; done

# 작업용 EC2 - 터미널1 , 파드 확인
watch -d 'kubectl get pods -o wide'

# 디플로이먼트 생성
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/2/nginx-dp.yaml
kubectl apply -f nginx-dp.yaml

# Pod 30개로 증설
kubectl scale deployment nginx-deployment --replicas=30

# Pod 50개로 증설
kubectl scale deployment nginx-deployment --replicas=50

 

 

Pod 를 50 개로 증설했을 때, Pending 상태로 배포가 되지 않는 것을 알 수 있습니다.
이는 ENI 개수 제한으로 인해 발생한 것입니다.

앞서 언급한 것처럼 이를 해결하기 위해서는 Prefix Delegation 과 Custom Network 를 사용하면 됩니다.



3. EKS LoadBalancer

 

EKS 는 AWS LoadBalancer Controller 를 통해 ELB 에서 Pod IP 로 Direct 통신할 수 있도록 만들 수 있습니다.

 

출처: kans study

 

통신 방식은 '인스턴스 유형'과 'IP 유형' 이 있는 데, 'IP 유형'이 위에서 언급한 통신 방식입니다.

 

인스턴스 유형 동작 방식

출처 : https://aws.amazon.com/ko/blogs/networking-and-content-delivery/deploying-aws-load-balancer-controller-on-amazon-eks/

 

IP 유형 동작 방식

출처 : https://aws.amazon.com/ko/blogs/networking-and-content-delivery/deploying-aws-load-balancer-controller-on-amazon-eks/

 

 

3.1. AWS LoadBalancer Controller 배포

 

# Helm Chart 설치
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME

# 작업용 EC2 - 디플로이먼트 & 서비스 생성
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/2/echo-service-nlb.yaml
cat echo-service-nlb.yaml

# 배포
kubectl apply -f echo-service-nlb.yaml

 

 

해당 YAML 파일 내용은 아래와 같습니다.

 

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-echo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: deploy-websrv
  template:
    metadata:
      labels:
        app: deploy-websrv
    spec:
      terminationGracePeriodSeconds: 0
      containers:
      - name: akos-websrv
        image: k8s.gcr.io/echoserver:1.5
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: svc-nlb-ip-type
  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-healthcheck-port: "8080"
    service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
spec:
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
  type: LoadBalancer
  loadBalancerClass: service.k8s.aws/nlb
  selector:
    app: deploy-websrv

 

AWS 콘솔에서 생성된 NLB 를 확인하면, Pod 가 대상에 붙어 있는 것을 알 수 있습니다.

 



3.2. LB 설정 편집

 

YAML 을 통해 AWS LB 의 설정을 변경할 수 있습니다.

대상그룹의 '등록 취소 지연' 설정이 현재는 300초입니다.

 

vi echo-service-nlb.yaml

# 마지막 deregistation_delay 설정 추가
..
apiVersion: v1
kind: Service
metadata:
  name: svc-nlb-ip-type
  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-healthcheck-port: "8080"
    service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
    service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: deregistration_delay.timeout_seconds=60
...

# 변경 적용
kubectl apply -f echo-service-nlb.yaml

 

AWS 콘솔에서 확인하면 변경된 것을 알 수 있습니다.

 

# 분산 접근 확인
NLB=$(kubectl get svc svc-nlb-ip-type -o jsonpath={.status.loadBalancer.ingress[0].hostname})
curl -s $NLB
for i in {1..100}; do curl -s $NLB | grep Hostname ; done | sort | uniq -c | sort -nr

 



3.3. ALB 배포

 

ALB 또한 NLB 와 크게 다르지 않습니다.

출처: kans study

 

curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/ingress1.yaml
cat ingress1.yaml
kubectl apply -f ingress1.yaml

 

해당 YAML 파일은 2048 게임을 배포하는 파드입니다.

 

apiVersion: v1
kind: Namespace
metadata:
  name: game-2048
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: game-2048
  name: deployment-2048
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: app-2048
  replicas: 2
  template:
    metadata:
      labels:
        app.kubernetes.io/name: app-2048
    spec:
      containers:
      - image: public.ecr.aws/l6m2t8p7/docker-2048:latest
        imagePullPolicy: Always
        name: app-2048
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  namespace: game-2048
  name: service-2048
spec:
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
  type: NodePort
  selector:
    app.kubernetes.io/name: app-2048
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: game-2048
  name: ingress-2048
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: service-2048
              port:
                number: 80

 

똑같이 ALB 대상그룹에는 Pod IP 가 있는 것을 알 수 있습니다.

 



4. Topology Aware Routing

 

Topology Aware Routing 은 같은 AZ 의 파드로만 접속할 수 있도록 하는 설정입니다.
이는 가용영역간 네트워크 비용을 감소시키는 데 효율적입니다.

 

Topology Mode(구 Aware Hint) 설정을 하면 됩니다.

출처: https://docs.aws.amazon.com/eks/latest/best-practices/cost-opt-networking.html

 

 

4.1. 서비스 배포

 

테스트를 위한 서비스를 배포합니다.

 

# 테스트를 위한 디플로이먼트와 서비스 배포
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-echo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: deploy-websrv
  template:
    metadata:
      labels:
        app: deploy-websrv
    spec:
      terminationGracePeriodSeconds: 0
      containers:
      - name: websrv
        image: registry.k8s.io/echoserver:1.5
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: svc-clusterip
spec:
  ports:
    - name: svc-webport
      port: 80
      targetPort: 8080
  selector:
    app: deploy-websrv
  type: ClusterIP
EOF

# 접속 테스트를 수행할 클라이언트 파드 배포
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: netshoot-pod
spec:
  containers:
  - name: netshoot-pod
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF

 

 

4.2. 테스트 파드 통신 테스트

 

# 디플로이먼트 파드가 배포된 AZ(zone) 확인
kubectl get pod -l app=deploy-websrv -owide

# 100번 반복 접속 : 3개의 파드로 AZ(zone) 상관없이 랜덤 확률 부하분산 동작
kubectl exec -it netshoot-pod -- zsh -c "for i in {1..100}; do curl -s svc-clusterip | grep Hostname; done | sort | uniq -c | sort -nr"

 

AZ 상관없이 랜덤으로 부하가 분산됩니다.

 

 

 

4.3. Topology Aware Routing 설정

 

# Topology Aware Routing 설정 : 서비스에 annotate에 아래처럼 추가
kubectl annotate service svc-clusterip "service.kubernetes.io/topology-mode=auto"

# 100번 반복 접속 : 테스트 파드(netshoot-pod)와 같은 AZ(zone)의 목적지 파드로만 접속
kubectl exec -it netshoot-pod -- zsh -c "for i in {1..100}; do curl -s svc-clusterip | grep Hostname; done | sort | uniq -c | sort -nr"

 

동일한 AZ 의 파드로만 트래픽을 보내는 것을 알 수 있습니다.

 

 

# endpointslices 확인 시, 기존에 없던 hints 가 추가되어 있음 >> 참고로 describe로는 hints 정보가 출력되지 않음
kubectl get endpointslices -l kubernetes.io/service-name=svc-clusterip -o yaml

 

 

 

4.4. AZ 에 파드가 없을 경우

 

같은 AZ 에 파드가 없을 경우 어떻게 동작할까요??

# 파드 갯수를 1개로 줄이기
kubectl scale deployment deploy-echo --replicas 1

# 100번 반복 접속 : 다른 AZ이지만 목적지파드로 접속됨!
kubectl exec -it netshoot-pod -- zsh -c "for i in {1..100}; do curl -s svc-clusterip | grep Hostname; done | sort | uniq -c | sort -nr"

 

다른 AZ 지만, 접속이 가능합니다.
이는 ExternalPolicy Local 의 동작방식과 다릅니다.
ExternalPolicy Local 의 경우, 목적지 파드가 없다면 오류를 내뱉지만 Topology Aware Routing 의 경우 서비스 가용성을 위해 다른 AZ 라도 통신이 가능하게 설정되어 있습니다.

 

 


 

이로써 9주간의 스터디가 모두 마무리되었습니다.
역대급으로 긴 스터디로 엄청난 양의 스터디를 준비해주신 가시다님께 감사의 말씀 드립니다.

 

이번 스터디를 통해서 너무나도 많은 지식을 습득할 수 있었습니다.
스터디 중간 중간 개인적으로도 많은 일이 있었지만, 끝까지 포기하지 않고 완주했다는 점이 너무도 좋습니다.
이번 스터디는 절대 잊지 못할 것 같습니다.

 

Contents

포스팅 주소를 복사했습니다