EKS Networking

kimchigood·2023년 5월 1일
0

AEWS Study

목록 보기
2/11
post-thumbnail

본 포스팅은 CloudNet@팀의 AWES스터디 활동을 통한 포스팅임을 밝힌다. 이번 포스팅에서는 EKS Network에 대해 알아보겠다.

Preparation

스터디에서 제공해준 one-click 배포 스크립트를 통해 EKS를 배포해보자. 배포는 보통 20분정도 걸린다.

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

# CloudFormation 스택 배포
# aws cloudformation deploy --template-file eks-oneclick.yaml --stack-name myeks --parameter-overrides KeyName=<My SSH Keyname> SgIngressSshCidr=<My Home Public IP Address>/32 MyIamUserAccessKeyID=<IAM User의 액세스키> MyIamUserSecretAccessKey=<IAM User의 시크릿 > ClusterBaseName='<eks 이름>' --region ap-northeast-2
예시) aws cloudformation deploy --template-file eks-oneclick.yaml --stack-name myeks --parameter-overrides KeyName=kp-gasida SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32  MyIamUserAccessKeyID=AKIA5... MyIamUserSecretAccessKey='CVNa2...' ClusterBaseName=myeks --region ap-northeast-2

## Tip. 워커노드 인스턴스 타입 변경 : WorkerNodeInstanceType=t3.xlarge
예시) aws cloudformation deploy --template-file eks-oneclick.yaml --stack-name myeks --parameter-overrides KeyName=kp-gasida SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32  MyIamUserAccessKeyID=AKIA5... MyIamUserSecretAccessKey='CVNa2...' ClusterBaseName=myeks --region ap-northeast-2 WorkerNodeInstanceType=t3.xlarge 

# CloudFormation 스택 배포 완료 후 작업용 EC2 IP 출력
aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text

# 마스터노드 SSH 접속
ssh -i ~/.ssh/kp-gasida.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)

1. AWS VPC CNI

CNI?

Container Network Interface로, 컨테이너간 통신을 할 수 있게 해주는 인터페이스로Calico, Flannel 등 여러 3rd Party들이 존재한다. CNI 자체도 CNCF의 프로젝트에 속한다.

AWS VPC CNI

AWS에서 사용하는 CNI로, Node와 Pod의 IP대역이 같은 특징이 있다. 대역이 같이 때문에 Pod에서 다른 Pod 또는 외부 통신할 때, 오버레이 네트워크를 건너뛰는 장점이 있다.

IP대역이 같기 때문에 패킷 전송 시 경로최적화, 디버깅, 통신효율성 등의 이점을 취한다.

1-1. Node와 Pod IP 대역확인

# Node 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

---------------------------------------------------------------------
|                         DescribeInstances                         |
+-------------------+-----------------+------------------+----------+
|   InstanceName    |  PrivateIPAdd   |   PublicIPAdd    | Status   |
+-------------------+-----------------+------------------+----------+
|  myeks-ng1-Node   |  192.168.3.75   |  15.164.231.37   |  running |
|  myeks-ng1-Node   |  192.168.2.128  |  3.38.176.32     |  running |
|  myeks-bastion-EC2|  192.168.1.100  |  13.124.132.143  |  running |
|  myeks-ng1-Node   |  192.168.1.218  |  3.35.149.217    |  running |
+-------------------+-----------------+------------------+----------+

# Pod IP
$ kubectl get pod -n kube-system -o=custom-columns=NAME:.metadata.name,IP:.status.podIP,STATUS:.status.phase

NAME                       IP              STATUS
aws-node-kgdfc             192.168.3.75    Running
aws-node-p659k             192.168.1.218   Running
aws-node-z49rm             192.168.2.128   Running
coredns-6777fcd775-54j4b   192.168.2.192   Running
coredns-6777fcd775-zz995   192.168.1.214   Running
kube-proxy-dggvv           192.168.3.75    Running
kube-proxy-ghjf4           192.168.2.128   Running
kube-proxy-kt6h6           192.168.1.218   Running

1-2. Node에서 네트워크 정보확인

Node에서 배포된 Pod를 확인하면서, 내부적으로는 어떻게 네트워크가 구성되는 지 알아보자.

AWS VPC CNI에서는 각 Node가 ENI를 통해 IP주소들을 관리한다. 위 그림에서처럼 기본적으로 ENI0이 존재하고, Node에 Pod가 배포되면 ENI1이 생성되고 관리되는 IP주소들도 늘어나게 된다.
아래 실습을 통해 확인해보자.

# 노드에 툴 설치
ssh ec2-user@$N1 sudo yum install links tree jq tcpdump -y
ssh ec2-user@$N2 sudo yum install links tree jq tcpdump -y
ssh ec2-user@$N3 sudo yum install links tree jq tcpdump -y

(pkos-admin@myeks:default) [root@myeks-bastion-EC2 ~]# k get po -A -owide
NAMESPACE     NAME                       READY   STATUS    RESTARTS   AGE   IP              NODE                                               NOMINATED NODE   READINESS GATES
kube-system   aws-node-kgdfc             1/1     Running   0          34m   192.168.3.75    ip-192-168-3-75.ap-northeast-2.compute.internal    <none>           <none>
kube-system   aws-node-p659k             1/1     Running   0          34m   192.168.1.218   ip-192-168-1-218.ap-northeast-2.compute.internal   <none>           <none>
kube-system   aws-node-z49rm             1/1     Running   0          34m   192.168.2.128   ip-192-168-2-128.ap-northeast-2.compute.internal   <none>           <none>
kube-system   coredns-6777fcd775-54j4b   1/1     Running   0          32m   192.168.2.192   ip-192-168-2-128.ap-northeast-2.compute.internal   <none>           <none>
kube-system   coredns-6777fcd775-zz995   1/1     Running   0          32m   192.168.1.214   ip-192-168-1-218.ap-northeast-2.compute.internal   <none>           <none>
kube-system   kube-proxy-dggvv           1/1     Running   0          33m   192.168.3.75    ip-192-168-3-75.ap-northeast-2.compute.internal    <none>           <none>
kube-system   kube-proxy-ghjf4           1/1     Running   0          33m   192.168.2.128   ip-192-168-2-128.ap-northeast-2.compute.internal   <none>           <none>
kube-system   kube-proxy-kt6h6           1/1     Running   0          33m   192.168.1.218   ip-192-168-1-218.ap-northeast-2.compute.internal   <none>           <none>
(pkos-admin@myeks:default) [root@myeks-bastion-EC2 ~]#            k get nodes -owide
NAME                                               STATUS   ROLES    AGE   VERSION                INTERNAL-IP     EXTERNAL-IP     OS-IMAGE         KERNEL-VERSION                  CONTAINER-RUNTIME
ip-192-168-1-218.ap-northeast-2.compute.internal   Ready    <none>   36m   v1.24.11-eks-a59e1f0   192.168.1.218   3.35.149.217    Amazon Linux 2   5.10.176-157.645.amzn2.x86_64   containerd://1.6.19
ip-192-168-2-128.ap-northeast-2.compute.internal   Ready    <none>   36m   v1.24.11-eks-a59e1f0   192.168.2.128   3.38.176.32     Amazon Linux 2   5.10.176-157.645.amzn2.x86_64   containerd://1.6.19
ip-192-168-3-75.ap-northeast-2.compute.internal    Ready    <none>   36m   v1.24.11-eks-a59e1f0   192.168.3.75    15.164.231.37   Amazon Linux 2   5.10.176-157.645.amzn2.x86_64   containerd://1.6.19

(pkos-admin@myeks:default) [root@myeks-bastion-EC2 ~]# ssh ec2-user@$N1 sudo ip -c route
default via 192.168.1.1 dev eth0
169.254.169.254 dev eth0
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.218
192.168.1.214 dev eni6822b7a32a0 scope link
(pkos-admin@myeks:default) [root@myeks-bastion-EC2 ~]# ssh ec2-user@$N2 sudo ip -c route
default via 192.168.2.1 dev eth0
169.254.169.254 dev eth0
192.168.2.0/24 dev eth0 proto kernel scope link src 192.168.2.128
192.168.2.192 dev eni5fdf6e8a992 scope link
(pkos-admin@myeks:default) [root@myeks-bastion-EC2 ~]# ssh ec2-user@$N3 sudo ip -c route
default via 192.168.3.1 dev eth0
169.254.169.254 dev eth0
192.168.3.0/24 dev eth0 proto kernel scope link src 192.168.3.75

Node1,2에는 coredns Pod가 존재하여, eni가 추가되었다. pod가 존재하지 않는 Node3은 eni가 없는 것을 볼 수 있다. AWS콘솔에서도 각 Node의 Network 탭을 조회해보면, 할당된 IP들의 형태가 다른 것을 알 수 있다.

1-3. Node간 Pod통신

tcp dump를 통해, Pod간 통신에 대해 알아보자. 위에서 설명한대로, AWS VPC CNI는 Node와 Pod의 대역기 같이 때문에 오버레이 통신을 하지 않을 것이다.
실습용 Pod를 생성해보자.

# 테스트용 파드 netshoot-pod 생성
cat <<EOF | kubectl create -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

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

# 파드 확인
kubectl get pod -o wide
kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP

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

# tcp dump 모니터링 상태
$sudo tcpdump -i any -nn icmp



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

Pod1 에서 Pod2로 패킷전송을 하면,

$ kubectl get pod -o wide
NAME                            READY   STATUS    RESTARTS   AGE    IP              NODE                                               NOMINATED NODE   READINESS GATES
netshoot-pod-7757d5dd99-8ktsl   1/1     Running   0          6m2s   192.168.1.167   ip-192-168-1-218.ap-northeast-2.compute.internal   <none>           <none>
netshoot-pod-7757d5dd99-j88z8   1/1     Running   0          6m2s   192.168.3.137   ip-192-168-3-75.ap-northeast-2.compute.internal    <none>           <none>
netshoot-pod-7757d5dd99-q2h7v   1/1     Running   0          6m2s   192.168.2.206   ip-192-168-2-128.ap-northeast-2.compute.internal   <none>           <none

[ec2-user@ip-192-168-1-218 ~]$ sudo tcpdump -i any -nn icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
05:59:44.049074 IP 192.168.1.167 > 192.168.3.137: ICMP echo request, id 33728, seq 1, length 64
05:59:44.049120 IP 192.168.1.167 > 192.168.3.137: ICMP echo request, id 33728, seq 1, length 64
05:59:44.050401 IP 192.168.3.137 > 192.168.1.167: ICMP echo reply, id 33728, seq 1, length 64
05:59:44.050482 IP 192.168.3.137 > 192.168.1.167: ICMP echo reply, id 33728, seq 1, length 64

예상대로, 오버레이 통신이 되지 않고, 각 Pod의 IP로 통신이 되는 것을 볼 수 있다. 참고로, 위 그림을 다시 보면 Pod 간 패킷 전송시 메인인터페이스인 ENI0(eth0)으로 통신하는 것을 볼 수 있다. 실제로 tcpdump를 eth1, eth0을 떠보면 eth1에는 tcp가 잡히지 않는다.

sudo tcpdump -i eth1 -nn icmp
sudo tcpdump -i eth0 -nn icmp


그림참조: https://github.com/aws/amazon-vpc-cni-k8s/blob/master/docs/cni-proposal.md

1-4. Pod에서 외부통신

Pod에서 외부통신은 iptables 룰에 의해 SNAT이 된다. SNAT이 되는 이유는 Pod는 Private IP만 가지고 있기 때문에 외부와 통신 시 응답을 받을 수 있는 Public IP가 필요하다. 따라서 kube-proxy로 세팅해준 iptables 룰에 따라 Node의 Public IP로 SNAT이 되는 것이다.

실습

(pkos-admin@myeks:default) [root@myeks-bastion-EC2 ~]# kubectl exec -it $PODNAME1 -- curl -s ipinfo.io/ip ; echo
3.35.149.217

Pod가 외부통신할 때 나가는 IP가 3.35.149.217이다. 이건 Node Public IP일 것이다. 그럼 콘솔을 통해 확인해보자.

Node에 직접 접근해서 iptables 룰이 어떻게 세팅되어 있는지 보면,

[ec2-user@ip-192-168-1-218 ~]$ sudo iptables -t nat -S | grep 'A AWS-SNAT-CHAIN'
-A AWS-SNAT-CHAIN-0 ! -d 192.168.0.0/16 -m comment --comment "AWS SNAT CHAIN" -j AWS-SNAT-CHAIN-1
-A AWS-SNAT-CHAIN-1 ! -o vlan+ -m comment --comment "AWS, SNAT" -m addrtype ! --dst-type LOCAL -j SNAT --to-source 192.168.1.218 --random-fully

Pod cidr인 192.168.0.0/16 대역이 아닐 경우 AWS-SNAT-CHAIN-1 룰로 보내지고, 이것은 Node의 Public IP로 SNAT되는 것을 확인할 수 있다.

2. Service

AWS VPC CNI 환경에서 Service는 어떻게 구현되는 지 알아보자. 지난 Kops Study 때와 똑같은 방법을 사용하는데, 복습차원에서 간단히 실습을 해보겠다.


AWS VPC CNI의 가장 큰 특징인 Node와 Pod의 IP 대역이 같다는 점 때문에 Loadbalancer type의 Service에서 Pod IP로 다이렉트 통신이 가능하다. 일반적으로는 iptables 룰을 통해서 트래픽을 분산키는데, 네트워크홉이 추가되고 iptables 룰도 타기 때문에 약간은 비효율적이다.

# OIDC 확인
aws eks describe-cluster --name $CLUSTER_NAME --query "cluster.identity.oidc.issuer" --output text
aws iam list-open-id-connect-providers | jq

# IAM Policy (AWSLoadBalancerControllerIAMPolicy) 생성
curl -o iam_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.4.7/docs/install/iam_policy.json
aws iam create-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json

# 생성된 IAM Policy Arn 확인
aws iam list-policies --scope Local
aws iam get-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy
aws iam get-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --query 'Policy.Arn'

# AWS Load Balancer Controller를 위한 ServiceAccount를 생성 >> 자동으로 매칭되는 IAM Role 을 CloudFormation 으로 생성됨!
# IAM 역할 생성. AWS Load Balancer Controller의 kube-system 네임스페이스에 aws-load-balancer-controller라는 Kubernetes 서비스 계정을 생성하고 IAM 역할의 이름으로 Kubernetes 서비스 계정에 주석을 답니다
eksctl create iamserviceaccount --cluster=$CLUSTER_NAME --namespace=kube-system --name=aws-load-balancer-controller \
--attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --override-existing-serviceaccounts --approve

## 서비스 어카운트 확인
kubectl get serviceaccounts -n kube-system aws-load-balancer-controller -o yaml | yh

# 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 \
  --set serviceAccount.create=false --set serviceAccount.name=aws-load-balancer-controller
  
# 작업용 EC2 - 디플로이먼트 & 서비스 생성
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/2/echo-service-nlb.yaml
cat echo-service-nlb.yaml | yh
kubectl apply -f echo-service-nlb.yaml

Nginx Ingress Controller를 설치할 때 처럼 helm을 통해 AWS Load Balancer Controller를 설치해준다. AWS VPC CNI의 특징 때문인지는 몰라도, AKS(Azure)와는 많이 다른 방법을 사용한다. (내가 기능을 제대로 안써서 모르는 것일 수 도 있다.)

어쨋든, 위 방법으로 설치를하면 IAM Policy를 통해 EKS에 권한을 할당해주고, Loadbalancer type의 Service가 생성되면 AWS의 EC2 Target Group에 매핑되는 Pod가 들어가게 된다.

(pkos-admin@myeks:default) [root@myeks-bastion-EC2 ~]# k get po -owide
NAME                           READY   STATUS    RESTARTS   AGE    IP              NODE                                               NOMINATED NODE   READINESS GATES
deploy-echo-5c4856dfd6-lfbxn   1/1     Running   0          3m9s   192.168.3.91    ip-192-168-3-75.ap-northeast-2.compute.internal    <none>           <none>
deploy-echo-5c4856dfd6-ltnqs   1/1     Running   0          3m9s   192.168.2.139   ip-192-168-2-128.ap-northeast-2.compute.internal   <none>           <none>

Pod IP와 target Group의 IP가 같다. 즉, Loadbalancer에서 바로 Pod로 트래픽이 Bypass 되는 것이 증명된다.

3. Ingress

Service와 크게 다를 게 없다. L7에서 작동하는 ALB를 통해서 Pod로 Bypass 되는데, HTTP/HTTPS 통신을 하게된다.

# 게임 파드와 Service, Ingress 배포
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/ingress1.yaml
cat ingress1.yaml | yh
kubectl apply -f ingress1.yaml

# 모니터링
watch -d kubectl get pod,ingress,svc,ep -n game-2048

# 생성 확인
kubectl get-all -n game-2048
kubectl get ingress,svc,ep,pod -n game-2048
kubectl get targetgroupbindings -n game-2048
NAME                               SERVICE-NAME   SERVICE-PORT   TARGET-TYPE   AGE
k8s-game2048-service2-e48050abac   service-2048   80             ip            87s

# Ingress 확인
kubectl describe ingress -n game-2048 ingress-2048

# 게임 접속 : ALB 주소로 웹 접속
kubectl get ingress -n game-2048 ingress-2048 -o jsonpath={.status.loadBalancer.ingress[0].hostname} | awk '{ print "Game URL = http://"$1 }'

# 파드 IP 확인
kubectl get pod -n game-2048 -owide
NAME                               READY   STATUS    RESTARTS   AGE     IP              NODE                                               NOMINATED NODE   READINESS GATES
deployment-2048-6bc9fd6bf5-2cwxf   1/1     Running   0          4m19s   192.168.2.216   ip-192-168-2-128.ap-northeast-2.compute.internal   <none>           <none>
deployment-2048-6bc9fd6bf5-nfcjn   1/1     Running   0          4m19s   192.168.1.231   ip-192-168-1-218.ap-northeast-2.compute.internal   <none>           <none>

Service와 마찬가지로 Pod IP가 target group에 바로 인입된다.

Wrap up

kops 스터디에서 똑같이 배웠던 내용인데, 너무 새록새록하다. 이번에는 AWS VPC CNI에 대해 조금은 더 친숙해진 느낌이다. 실무에서 AWS를 쓰지 않아서, 조금 헷갈리는 부분도 있는 것 같다. 시간내서 몇 번 더 복습하면 더 잘 이해가 갈 것 같기도 하다.

profile
Shout out to Kubernetes⎈

0개의 댓글