본문 바로가기
Kubernetes/PKOS

[2주차] VPC CNI와 LoadBalancer Controller

by isn`t 2023. 3. 19.

*본 게시글의 내용은 가시다님의 노션 페이지와 스터디 자료인 '24단계 실습으로 정복하는 쿠버네티스 도서' 를 기반으로 작성하였습니다.

AWS VPC CNI

)

  • AWS 환경에서 동작하는 Kops나 EKS를 위한 CNI
  • 온프레미스에 k8s를 구성하면 노드(호스트)의 네트워크 대역과 파드의 네트워크 대역이 다르다.
  • AWS VPC CNI는 노드와 파드가 같은 네트워크 대역을 사용하며, 각 파드에 eni가 연결되어 독립적인 IP를 할당받아 사용할 수 있다.
  • 단, 파드의 개수가 많아지면 많아질수록 각 서브넷에서 가용한 IP의 수가 그만큼 소모되고, 노드의 역할을 하는 EC2가 가질 수 있는 ENI당 Secondary IP의 리밋도 있기 때문에 사전에 고려가 필요하다.
  • 단, Kops는 아직 PodSecurityGroup을 지원하지는 않는다. 현재는 EKS에서만 가능.
  • kube-proxy와 aws-node 파드 등 몇몇 파드는 노드와 동일한 IP를 가진다. 때문에 ENI Secondary IP 갯수를 계산할 때 포함하지 않는다.
[root@kops-ec2 ~]# kubectl get node -o wide
NAME                  STATUS   ROLES           AGE   VERSION    INTERNAL-IP     EXTERNAL-IP    OS-IMAGE             KERNEL-VERSION    CONTAINER-RUNTIME
i-01493969e37859769   Ready    control-plane   35m   v1.24.11   172.30.42.219   3.36.54.165    Ubuntu 20.04.5 LTS   5.15.0-1031-aws   containerd://1.6.18
i-0e56757db751fb4e9   Ready    node            32m   v1.24.11   172.30.63.228   13.209.10.83   Ubuntu 20.04.5 LTS   5.15.0-1031-aws   containerd://1.6.18

[root@kops-ec2 ~]kubectl get pod -o wide -A
...
kube-system   aws-node-gf6hx                                  1/1     Running   0             36m   172.30.42.219   i-01493969e37859769   <none>           <none>
kube-system   aws-node-vxf5r                                  1/1     Running   0             34m   172.30.63.228   i-0e56757db751fb4e9   <none>           <none>

...

kube-system   kube-proxy-i-01493969e37859769                  1/1     Running   0             35m   172.30.42.219   i-01493969e37859769   <none>           <none>
kube-system   kube-proxy-i-0e56757db751fb4e9                  1/1     Running   0             34m   172.30.63.228   i-0e56757db751fb4e9   <none>           <none>
...
...

node의 IP 라우팅 정보를 보면, veth 인터페이스로 걸려있는 IP들이 VPC CNI에 물려있는 파드의 IP이다.

ubuntu@i-01493969e37859769:~$ ip -c route
default via 172.30.32.1 dev ens5 proto dhcp src 172.30.42.219 metric 100 
172.30.32.0/19 dev ens5 proto kernel scope link src 172.30.42.219 
172.30.32.1 dev ens5 proto dhcp scope link src 172.30.42.219 metric 100 
172.30.60.80 dev enif9dc7364ce2 scope link 
172.30.60.81 dev eni4d9edc11021 scope link 
172.30.60.82 dev enia699f5ca146 scope link 
172.30.60.83 dev eni35b0ddd5b59 scope link

---

[root@kops-ec2 ~]# kubectl get pod -A -o wide | grep "172.30.60.80"
kube-system   ebs-csi-node-xzdbn                              3/3     Running   0             40m   172.30.60.80    i-01493969e37859769   <none>           <none>

파드를 생성하면, 파드가 배치된 노드의 라우팅 정보에서도 추가된 파드의 IP를 확인할 수 있다.

(ilikebigmac:default) [root@kops-ec2 ~]# kubectl get pod -o wide
columns=NAME:.metadata.name,IP:.status.podIPNAME                            READY   STATUS    RESTARTS   AGE   IP              NODE                  NOMINATED NODE   READINESS GATES
netshoot-pod-7757d5dd99-cvggz   1/1     Running   0          88s   172.30.57.181   i-0e56757db751fb4e9   <none>           <none>
netshoot-pod-7757d5dd99-r4lnj   1/1     Running   0          88s   172.30.57.182   i-0e56757db751fb4e9   <none>           <none>

생성된 파드 내에서 exec으로 접근해도 같은 172.30.57.181 IP를 확인할 수 있다.

netshoot-pod-7757d5dd99-cvggz# ip -c route
default via 169.254.1.1 dev eth0 
169.254.1.1 dev eth0 scope link 
netshoot-pod-7757d5dd99-cvggz# ip -c addr 
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
3: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state UP group default 
    link/ether 7e:9b:0d:58:45:f9 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.30.57.181/32 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::7c9b:dff:fe58:45f9/64 scope link 
       valid_lft forever preferred_lft forever

파드간 통신

확인해 본 바와 같이 AWS VPC CNI를 이용하면 파드는 노드와 같은 IP 대역을 할당받게 된다.

노드는 EC2로 구성되어 있으므로 노드와 파드는 동일한 VPC 내에 위치한다는 의미이다.

그리고 VPC의 대역은 앞서 확인한 파드와 노드의 IP 대역인 172.30으로 시작하는 대역이다.

따라서 서로 같은 VPC 내에 위치한 모든 파드들은 NAT를 통하지 않더라도 VPC 대역에 해당하는 Private IP로 통신을 할 수 있게 된다.

파드의 외부 네트워크 통신

netshoot-pod-7757d5dd99-cvggz# ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=104 time=22.7 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=104 time=22.6 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=104 time=22.6 ms

파드 내에서 구글 DNS로 ping을 날려보면 정상적으로 응답을 주고받을 수 있다. 그렇다면 외부 통신은 어떻게 될까?

ubuntu@i-0e56757db751fb4e9:~$ sudo iptables -t nat -S | grep 'A AWS-SNAT-CHAIN'
-A AWS-SNAT-CHAIN-0 ! -d 172.30.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 172.30.63.228 --random-fully

워커 노드에서 확인해보면, 파드가 외부와 통신할 때는 AWS-SNAT-CHAIN 에 의해 EC2의 Public IP로 SNAT되어서 외부와 통신하게 된다.

k8s Service & AWS LoadBalancer Controller

AWS LoadBalancer Controller는 ELB의 Target Group에 타겟으로 연결된 Pod의 정보를 지속적으로 제공하는 역할을 한다.

k8s Service를 LoadBalancer Type으로 생성하면 타겟을 Instance와 IP 타입 중 선택할 수 있는데, IP 타입으로 구성하게 되면 ELB는 LoadBalancer Controller에 의해 Pod의 IP를 동적으로 제공받게 된다.

따라서 Client-ELB-Pod로 효율적인 네트워크를 구성할 수 있다. 만약 Instance Type을 선택하게 되면 기존에 사용하던 NodePort 방식으로 구성된다.

AWS LoadBalancer Conteroller Docs

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-websr

AWS LoadBalancer Conterller Annotations

Service metadata에 기입된 annotations는 어떤 방식으로 ELB에 서비스와 파드를 매핑하여 구성할 것인지 정의하게 된다.

위 링크에서 NLB/ALB의 각기 다른 annotation을 확인할 수 있다.

위의 서비스는 NLB에 ip타입으로 매핑하며 cross zone load balancing 옵션을 enable 하고, internt-facing 이므로 외부에서 브라우저로 접근해도 위와 같이 서비스에 접근이 가능하다.

이와 같은 구성으로 AWS 환경에 NLB가 생성된다.

internet-facing이므로 일반 환경에서도 생성된 NLB의 DNS Name으로 접속해보면 위와 같은 결과를 확인할 수 있다.

연결된 타겟그룹에는 172.30.57.181, 172.30.57.182 두 개의 IP와 8080포트로 매핑되어 있다.

(ilikebigmac:default) [root@kops-ec2 ~]# kubectl get pod -o wide
NAME                           READY   STATUS    RESTARTS   AGE     IP              NODE                  NOMINATED NODE   READINESS GATES
deploy-echo-5c4856dfd6-bhvjm   1/1     Running   0          8m25s   172.30.57.181   i-0e56757db751fb4e9   <none>           <none>
deploy-echo-5c4856dfd6-g7vht   1/1     Running   0          8m25s   172.30.57.182   i-0e56757db751fb4e9   <none>           <none>

그리고 이들 IP는 앞서 echo-server deployment에 의해 배포된 파드의 IP임을 확인 가능하다.

Ingress

인그레스는 클러스터 내부의 서비스(ClusterIP, NodePort, Loadbalancer)를 HTTP/HTTPS로 외부에 노출시키는 Web Proxy의 역할을 한다.

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

마찬가지로 annotation에 의해 ip타입의 internet-facing ALB가 AWS 환경에 생성된다.

 [root@kops-ec2 ~]# kubectl get pod -n game-2048 -owide
NAME                               READY   STATUS    RESTARTS   AGE     IP              NODE                  NOMINATED NODE   READINESS GATES
deployment-2048-6bc9fd6bf5-c64wr   1/1     Running   0          4m14s   172.30.55.145   i-0e56757db751fb4e9   <none>           <none>
deployment-2048-6bc9fd6bf5-ks2v4   1/1     Running   0          4m14s   172.30.55.144   i-0e56757db751fb4e9   <none>           <none>

타겟 역시 IP타입으로 매핑되어 파드의 IP와 동일하며, 80포트로 매핑되어 있다.

ALB의 DNS Name으로 접근하면 호스팅 중인 웹 서비스에 접근이 가능하다.

이와 같은 웹 서비스를 배포하는 상황을 가정하고, 생성된 ALB에 ExternalDNS를 albweb.ilikebigamc.link 로 설정하였으며, 정상 접근됨을 확인하였다.

DNS 레코드와 등록 정보를 살펴보면

(ilikebigmac:default) [root@kops-ec2 ~]# curl albweb.ilikebigmac.link -vvv
*   Trying 3.39.77.155:80...
* Connected to albweb.ilikebigmac.link (3.39.77.155) port 80 (#0)
> GET / HTTP/1.1
> Host: albweb.ilikebigmac.link
> User-Agent: curl/7.88.1
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Sat, 18 Mar 2023 20:20:08 GMT
< Content-Type: text/html
< Content-Length: 3988
< Connection: keep-alive
< Server: nginx
< Last-Modified: Wed, 06 Oct 2021 17:35:37 GMT
< ETag: "615dde69-f94"
< Accept-Ranges: bytes
< 

...

* Connection #0 to host albweb.ilikebigmac.link left intac

(ilikebigmac:default) [root@kops-ec2 ~]# dig albweb.ilikebigmac.link

; <<>> DiG 9.11.4-P2-RedHat-9.11.4-26.P2.amzn2.5.2 <<>> albweb.ilikebigmac.link
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 36674
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;albweb.ilikebigmac.link.       IN      A

;; ANSWER SECTION:
albweb.ilikebigmac.link. 21     IN      A       3.39.208.51
albweb.ilikebigmac.link. 21     IN      A       3.39.77.155

;; Query time: 0 msec
;; SERVER: 10.0.0.2#53(10.0.0.2)
;; WHEN: Sun Mar 19 05:20:41 KST 2023
;; MSG SIZE  rcvd: 84

3.39.208.513.39.77.155 라는 두 개의 IP를 돌려주는데 이를 ENI에서 조회해보면 ALB의 Public IP임을 알 수 있다.

따라서 지금까지 확인한 내용을 바탕으로 `albweb.ilikebigmac.link` 에 접속할 때 클라이언트는 Route53(External DNS) -> ALB -> Pod의 흐름으로 페이지에 접근하게 됨을 알 수 있었다.

이 때 Route53에 등록되는 도메인은 ExternalDNS에 의헤 레코드 관리되고, LoadBalancer Controller는 동적으로 변화하는 Pod의 IP를 지속적으로 ELB에 제공한다.

ELB는 k8s Service 명세의 annotation에 매핑된 정보를 바탕으로 생성한 ELB에 타겟 그룹을 연결, 타겟 그룹에는 전달받은 Pod의 IP/Port 가 타겟으로 등록된다.

EKS에서 VPC CNI를 사용하는 경우 이러한 구조 덕에Pod에 직접 보안 그룹을 적용할 수 있으며, Node와 Pod는 동일 네트워크 대역을 가지고 이 대역은 VPC CIDR에 해당한다.

각 Pod의 IP는 ENI의 Secondary IP로 등록되는데, EC2의 Instance Type에 따라 등록될 수 있는 Pod IP의 갯수에 제한이 있고, 각 서브넷에서도 available IP에 제한이 있으므로 인스턴스별, 서브넷별로 배치할 파드의 갯수가 적절한지 배포 전에 검증해볼 필요가 있다.