[AWS EKS Workshop Study] 2주차 - EKS 네트워크

JoonHyeok Han·2024년 3월 15일
0
post-thumbnail

개요

2주차에서는 쿠버네티스에서 이루어지는 네트워크 통신을 주제로 아래와 같은 내용을 학습한다.

  • 쿠버네티스에서 서로 다른 물리적 PC 에 위치한 파드끼리 어떻게 통신할 수 있는가? (CNI)
  • 클러스터 외부(인터넷)과 파드는 어떻게 통신하는가?
  • 노드에 파드를 최대 몇 개까지 생성할 수 있는가?
  • 클러스터의 트래픽을 분산시켜주는 서비스와 로드 밸런서에 대해 알아보고, EKS 와 함께 사용할 수 있는 AWS LoadBalancer Controller 에 대해 알아본다.

EKS 클러스터 환경은 가시다님께서 실습을 편리하게 진행하기 위해 배포해주신 CloudFormation 으로 아래의 이미지처럼 구성했다.

출처: 스터디 공유자료
출처: 스터디 공유자료

VPC 1개에 퍼블릭 서브넷 3개, 프라이빗 서브넷 3개가 생성됐다.

서울 리전의 3개 가용 영역에 걸쳐 워커 노드(EC2)가 각각 1개씩 실행하고 있다.

쿠버네티스 명령어를 사용하기 위해 배스천 호스트도 1개 실행하고 있다.

  • 참고로 배스천(Bastion) 호스트란 특정 네트워크의 내부로 접속하기 위해 오직 증명된 사람만 접속할 수 있는 PC 를 의미한다.
  • 배스천이라는 말이 와닿지 않는다면 블리자드에서 출시한 게임 <오버워치>에 바스티온이라는 캐릭터를 생각하면 된다. 배스천의 뜻은 포루(포를 쏘는 요새)인데 과거에 외부의 적들이 침입하기 어렵게 하면서 성을 지키기 위해 포루를 만들었다고 한다. 아래의 이미지는 <오버워치>의 바스티온 캐릭터이다. bastion

CNI 란?

CNI 는 Container Network Interface 의 약자로 컨테이너 간 네트워크를 제어할 수 있는 플러그인을 만들기 위한 표준이다.

다양한 컨테이너 런타임과 오케스트레이터 사이의 네트워크 계층을 구현하는 방식이 다양하게 분리되는 것을 방지하고 공통된 인터페이스를 제공하기 위해 CNCF 에서 관리하는 프로젝트이다.

쿠버네티스에서는 파드 간의 통신을 가능하게 하기 위해 CNI 를 사용한다.

쿠버네티스는 기본적으로 kubenet 이라는 CNI 플러그인을 제공하지만, 네트워크 기능이 제한적인 단점이 있다.

이를 보완하기 위해 서드파티(3rd-party) 플러그인을 사용하는데, 대표적으로는 Flannel, Calico, Weavenet 등이 있다.

CNI 가 필요한 이유?

아래의 구조와 같이 마스터 노드 1개, 워커 노드 2개가 있다고 해보자.

UI 컨테이너는 워커 노드 1번에서 실행 중이고, Login 컨테이너와 Cart 컨테이너는 워커 노드 2번에서 실행 중이다.

출처: [#1. Kubernetes 시리즈] 3. CNI란? (Container Network Interface) [티스토리]

정상적인 경우

다른 노드에 있는 컨테이너와 정상적으로 통신을 할 수 있다고 가정해보자.

워커 노드 1번의 UI 컨테이너(172.17.0.2)에서 워커 노드 2번의 Login 컨테이너(172.17.0.2)로 통신하기 위해 트래픽을 보낸다고 할 때, 아래의 순서를 거칠 것이다.

  1. UI 컨테이너의 veth0 인터페이스에서 docker0 브릿지 인터페이스(172.17.0.1)를 타고 NAT 처리를 한다.
  2. 워커 노드 1번의 물리 인터페이스인 ens160(10.200.155.22)로 이동한다.
  3. 워커 노드 2번의 물리 인터페이스인 ens160(10.200.155.23)으로 이동하고, docker0 브릿지 인터페이스(172.17.0.1)를 통해 Login 컨테이너의 veth0(172.17.0.2)로 이동한다.

CNI 가 없으면 생기는 문제

오버레이 네트워크와 같이 다른 노드와 통신을 할 수 있는 설정을 별도로 하지 않았다면, UI 컨테이너는 Login 컨테이너의 IP 주소인 172.17.0.2 로 패킷을 보낼 것이다.

워커 노드 1번의 docker0 브릿지에서 Destination IP 를 확인해보니 172.17.0.2 인데, 해당 IP 는 워커 노드 1번의 UI 컨테이너 IP 에 해당하기 때문에 다시 UI 컨테이너로 트래픽을 돌려보낼 것이다.

즉, UI 컨테이너와 Login 컨테이너의 IP 주소가 같기 때문에 다른 노드에 있는 컨테이너와 정상적으로 통신할 수 없는 문제가 생긴다.

이 문제를 해결하기 위해 CNI 를 사용한다.

CNI 적용 후

출처: [#1. Kubernetes 시리즈] 3. CNI란? (Container Network Interface) [티스토리]

위의 그림과 같이 CNI 가 노드 전체에 걸쳐 브릿지 인터페이스를 생성하고, 컨테이너의 네트워크 대역을 나눈다.

그리고 라우팅 테이블까지 생성해서 UI 컨테이너에서 Login 컨테이너, Cart 컨테이너로 통신할 수 있도록 한다.

즉, 서로 다른 물리 PC 들이 하나의 네트워크 안에서 통신하는 것처럼 가능하게 해주는 것이다.

네트워크 모델

CNI 에서 사용하는 네트워크 모델은 VXLAN(Virtual Extensible LAN), IP-in-IP 와 같은 캡슐화된 네트워크 모델이나 BGP(Border Gateway Protocol)와 같이 캡슐화 되지 않은 네트워크 모델을 사용한다.

실습 환경 확인

kube-proxy 작동 방식을 확인하기 위해 아래의 명령어를 실행했다.

kubectl describe cm -n kube-system kube-proxy-config \
| grep mode

실행 결과는 아래와 같다.

mode: "iptables"

노드들의 사설 IP 와 공인 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.53   |  52.79.37.199 |  running |
|  myeks-bastion-EC2 |  192.168.1.100  |  3.36.123.4   |  running |
|  myeks-ng1-Node    |  192.168.1.78   |  3.36.50.6    |  running |
|  myeks-ng1-Node    |  192.168.2.161  |  3.35.15.149  |  running |
+--------------------+-----------------+---------------+----------+

각 노드의 네트워크 인터페이스를 확인하기 위해 아래의 명령어를 실행했다.

for i in $N1 $N2 $N3; \
 do echo ">> node $i <<"; \
 ssh ec2-user@$i sudo ip -br -c addr; \
 echo; \
done

실행 결과는 아래와 같다.

>> node 192.168.1.78 <<
lo               UNKNOWN        127.0.0.1/8 ::1/128
eth0             UP             192.168.1.78/24 fe80::5:e1ff:fe8d:681/64
eni25c678c4d00@if3 UP             fe80::a0d4:46ff:fed5:304/64
eth1             UP             192.168.1.88/24 fe80::3:14ff:fe33:acf5/64

>> node 192.168.2.161 <<
lo               UNKNOWN        127.0.0.1/8 ::1/128
eth0             UP             192.168.2.161/24 fe80::405:28ff:fe6e:6db/64

>> node 192.168.3.53 <<
lo               UNKNOWN        127.0.0.1/8 ::1/128
eth0             UP             192.168.3.53/24 fe80::852:2aff:fe42:234d/64
eni275a7825693@if3 UP             fe80::4058:39ff:fe07:91/64
eth1             UP             192.168.3.136/24 fe80::8b5:4eff:fe84:cbd5/64

노드의 네트워크 환경

EKS 네트워크 구성

출처: 스터디 공유 자료

kube-proxy 와 aws-node 와 같은 특정 파드는 호스트의 IP 를 그대로 사용한다.

hostNetwork 옵션을 사용하면 파드가 호스트 IP 를 사용할 수 있는데, 쿠버네티스 공식 홈페이지(링크)에서는 해당 옵션을 가능하면 사용하지 않을 것을 권고하고 있다.

Avoid using hostNetwork, for the same reasons as hostPort.

이 옵션을 사용하면 파드가 호스트의 네트워크 네임스페이스를 공유하기 때문에 파드에 추가로 IP 를 할당하거나 별도의 네트워크를 구성하지 않아도 된다.

그리고 파드가 호스트의 네트워크 인터페이스를 직접 사용하기 때문에 네트워크 트래픽이 호스트와 직접 연결되어 라우팅이 간소해지는 장점이 있다.

AWS ENI(Elastice Network Interface)는 VPC 에서 가상 네트워크 카드를 나타내는 논리적 네트워크 구성 요소이다. ENI 는 인스턴스가 AWS 의 서비스들과 통신할 수 있도록 도와준다.

ENI 는 보조 프라이빗 IP 를 가질 수 있는데, 보조 프라이빗 IP 는 하나의 인스턴스가 여러 개의 IP 주소를 사용할 수 있는 것을 가능하게 한다. 이를 통해 다양한 애플리케이션을 하나의 인스턴스에서 호스팅하고, 각각의 애플리케이션에 대해 별도의 IP 주소를 제공하는 것이 가능해진다.

보조 프라이빗 IP 는 클라우드 서비스 업체에서 가상화 기술로 제공하는 것이며, 인스턴스의 유형에 따라 할당 받을 수 있는 IP 개수가 달라진다.

t3.medium 는 ENI 마다 최대 6개의 IP 를 할당 받을 수 있다.

kube-proxy

출처: [Worker Node] Kube-Proxy [velog]

kube-proxy 는 워커 노드에서 실행되며, 쿠버네티스 클러스터의 서로 다른 노드 간 파드끼리 통신이 가능하도록 네트워크 동작을 관리하는 컴포넌트이다.

노드에서 항상 실행 되어야 할 특정 파드를 관리하는 Daemonset 형태로 실행된다.

kube-proxy 는 클러스터에 새로운 Cluster IP 가 생성되거나 Pod 가 추가되면 현재 노드의 iptables 에 규칙을 추가한다.

이를 통해 노드나 Pod 가 교체되어도 동적으로 통신이 가능하도록 한다.

CoreDNS

CoreDNS 는 쿠버네티스 클러스터 DNS 서버 역할을 한다.

CoreDNS 를 이용하면 다른 서비스들의 IP 와 포트 정보를 저장하고 관리하기 위한 서비스 디스커버리(Service Discovery)를 가능하게 해준다.

이를 통해 클러스터 내부의 파드와 서비스에 대한 도메인 이름을 저장하고 네트워크 통신을 용이하게 해준다.

라우팅 테이블 동적 생성 확인 실습

워커 노드에서 라우팅 테이블이 동적으로 생성되는지 확인해보자.

워커 노드에 ssh 로 접속해서 라우팅 테이블 파일을 모니터링 하기 위해 아래의 명령어를 실행했다.

watch -d "ip link | egrep 'eth|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"

처음 실행 결과는 아래와 같다.

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP mode DEFAULT gro
up default qlen 1000
    link/ether 02:05:e1:8d:06:81 brd ff:ff:ff:ff:ff:ff
3: eni25c678c4d00@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state
UP mode DEFAULT group default
    link/ether a2:d4:46:d5:03:04 brd ff:ff:ff:ff:ff:ff link-netns cni-13084a9b-2c47-9d
8c-2b80-c91cb006f812
4: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP mode DEFAULT gro
up default qlen 1000
    link/ether 02:03:14:33:ac:f5 brd ff:ff:ff:ff:ff:ff

[ROUTE TABLE]
192.168.1.90    0.0.0.0         255.255.255.255 UH    0      0        0 eni25c678c4d00

이후 배스천 호스트에서 netshoot 이미지를 파드에 배포하는 명령어를 실행했다.

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

파드의 실행 상태를 확인하기 위해 아래의 명령어를 실행했다.

kubectl get pod -o wide

IP 항목을 보면 192.168.1.234(워커 노드1), 192.168.2.9(워커 노드2), 92.168.3.28(워커 노드3)으로 표시되어 있다.

파드 3개가 노드 3개에 걸쳐 1개씩 배포된 것을 확인할 수 있다.

NAME                            READY   STATUS    RESTARTS   AGE   IP              NODE                                               NOMINATED NODE   READINESS GATES
netshoot-pod-79b47d6c48-8fpng   1/1     Running   0          31s   192.168.2.9     ip-192-168-2-161.ap-northeast-2.compute.internal   <none>           <none>
netshoot-pod-79b47d6c48-b2tqn   1/1     Running   0          31s   192.168.1.234   ip-192-168-1-78.ap-northeast-2.compute.internal    <none>           <none>
netshoot-pod-79b47d6c48-rthkr   1/1     Running   0          31s   192.168.3.28    ip-192-168-3-53.ap-northeast-2.compute.internal    <none>           <none>

그리고 워커 노드 1의 라우팅 테이블은 아래와 같이 변경되었다.

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP mode DEFAULT gro
up default qlen 1000
    link/ether 02:05:e1:8d:06:81 brd ff:ff:ff:ff:ff:ff
3: eni25c678c4d00@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state
UP mode DEFAULT group default
    link/ether a2:d4:46:d5:03:04 brd ff:ff:ff:ff:ff:ff link-netns cni-13084a9b-2c47-9d
8c-2b80-c91cb006f812
4: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP mode DEFAULT gro
up default qlen 1000
    link/ether 02:03:14:33:ac:f5 brd ff:ff:ff:ff:ff:ff
5: enia5f43dc2393@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state
UP mode DEFAULT group default
    link/ether 9a:36:1a:c1:15:6c brd ff:ff:ff:ff:ff:ff link-netns cni-8bd26830-6d08-ec
a6-eb10-c88a33130221

[ROUTE TABLE]
192.168.1.90    0.0.0.0         255.255.255.255 UH    0      0        0 eni25c678c4d00
192.168.1.234   0.0.0.0         255.255.255.255 UH    0      0        0 enia5f43dc2393

워커 노드 1번에 배포한 netshoot 의 파드 IP 주소(192.168.1.234)가 라우팅 테이블에 추가된 것을 확인할 수 있다.

[ROUTE TABLE]
192.168.1.90    0.0.0.0         255.255.255.255 UH    0      0        0 eni25c678c4d00
192.168.1.234   0.0.0.0         255.255.255.255 UH    0      0        0 enia5f43dc2393

또한, 파드 내부와 통신하기 위해 아래의 네트워크 인터페이스가 추가된 것을 확인할 수 있다.

5: enia5f43dc2393@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state
UP mode DEFAULT group default
    link/ether 9a:36:1a:c1:15:6c brd ff:ff:ff:ff:ff:ff link-netns cni-8bd26830-6d08-ec
a6-eb10-c88a33130221

실제로 파드 내부에 네트워크 인터페이스가 추가되었는지 확인해보자.

  1. 워커 노드1 에서 마지막으로 생성된 네트워크 네임스페이스의 PID 를 확인한다.

    sudo lsns -o PID,COMMAND -t net \
    | awk 'NR>2 {print $1}' | tail -n 1
    #33994
  2. PID 를 환경 변수로 저장한다.

    MyPID=$(sudo lsns -o PID,COMMAND -t net \
    | awk 'NR>2 {print $1}' | tail -n 1)
  3. 파드의 네임스페이스로 접속해서 네트워크 인터페이스를 확인한다.

    sudo nsenter -t $MyPID -n 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@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state UP group default
        link/ether 7e:1c:ef:11:b7:55 brd ff:ff:ff:ff:ff:ff link-netnsid 0
        inet 192.168.1.234/32 scope global eth0
           valid_lft forever preferred_lft forever
        inet6 fe80::7c1c:efff:fe11:b755/64 scope link
           valid_lft forever preferred_lft forever

파드의 네트워크 인터페이스에서 @if5 가 의미하는 것은 워커 노드의 네트워크 인터페이스 5번에 연결되었다는 것을 의미한다.

3: eth0@if5: ...

그리고 워커 노드의 네트워크 인터페이스의 5번을 살펴보면 @if3 이 있는데, 이는 파드의 네트워크 인터페이스 3번에 연결되어있다는 것을 의미한다.

5: enia5f43dc2393@if3 ...

즉, 파드와 워커 노드에 네트워크 인터페이스가 연결되면서 노드는 내부 파드에 트래픽을 전달해줄 수 있게 되는 것이다.

또한, 워커 노드의 라우팅 테이블에 파드의 IP 가 추가되기 때문에 동적으로 생성되고 사라지는 파드의 정보를 업데이트하여 서비스 디스커버리를 가능하게 한다.

노드의 파드 최대 생성 개수

Prefix Delegation (접두사 위임)

쿠버네티스의 노드에는 파드를 생성할 수 있는 최대 개수는 노드의 성능(CPU, 메모리)에 따라 결정되지만, 할당 받을 수 있는 IP 의 개수에 따라서도 결정이 된다.

앞서 EC2 인스턴스의 유형에 따라 할당 받을 수 있는 보조 프라이빗 IP 의 개수가 정해진다고 했다.

t3.medium 을 기준으로는 1개의 ENI 에 최대 6개의 IP 가 할당 가능하다. 그래서 자기 자신을 제외한 나머지 5개의 IP 는 보조 프라이빗 IP 가 되는 것이다.

t3.medium 은 ENI 를 최대 3개 할당할 수 있는데, 그럼 최대 15개(3*5)의 IP 를 할당할 수 있는 것이다.

즉, t3.medium 을 기준으로는 하나의 노드에서는 파드를 최대 15개만 실행할 수 있는 것이다.

대신, 현재 네트워크 구성에서 kube-proxy 와 aws-node 는 호스트 IP 를 함께 사용하기 때문에 하나의 노드에서 총 17개의 파드를 사용할 수 있게 된다.

하지만 하나의 노드에서 파드 17개는 대규모 애플리케이션을 실행하기에는 턱없이 부족하다.

현재는 노드가 3개인데, 최대 51개 파드를 실행할 수 있는 것이다.

이를 해결하기 위해 prefix delegation(접두사 위임)을 사용한다.

접두사 위임은 서브넷 마스크를 이용해서 서브넷팅을 하는 개념과 비슷하다고 보면 된다.

예를 들어, 노드로 사용할 t3.medium EC2 인스턴스에는 1개의 ENI 에 최대 6개 IP 가 할당 가능하고, 기본 IP 주소를 제외한 나머지 5개에 대해 prefix 를 적용할 수 있는 것이다.

접두사 위임을 이용해서 서브넷팅 /28 을 적용하면 아래와 같이 대역을 나눌 수 있다.

번호CIDR 표기네트워크 주소브로드캐스트 주소
1192.168.2.16/28192.168.2.16192.168.31
2192.168.2.32/28192.168.2.32192.168.47
3192.168.2.48/28192.168.2.48192.168.63
4192.168.2.64/28192.168.2.64192.168.79
5192.168.2.80/28192.168.2.80192.168.95

1개의 서브넷은 네트워크 주소와 브로드캐스트 주소를 제외하면 14개 IP 를 할당 가능하며, ENI 1개에는 5*14 = 60개의 IP 를 할당할 수 있는 것이다.

즉, prefix 를 이용하면 t3.medium 에서는 파드를 최대 60개까지 생성할 수 있다.

실습

노드의 파드 최대 개수 생성 제한을 확인하기 위해 kube-ops-view 를 이용할 것이다.

prefix 모드를 적용하지 않았기 때문에 하나의 노드에서 최대 17개 파드를 실행할 수 있으며, 테스트를 위해 Nginx 파드를 총 50개 실행할 것이다.

아래의 명령어를 실행해서 Helm 차트 저장소를 추가한다.

helm repo add geek-cookbook https://geek-cookbook.github.io/charts/

아래의 명령어를 실행해서 kube-ops-view 의 Helm 차트를 설치한다.

helm install kube-ops-view geek-cookbook/kube-ops-view \
--version 1.2.2 \
--set env.TZ="Asia/Seoul" \
--namespace kube-system

배포된 주소를 확인하기 위해 아래의 명령어를 실행했다.

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"}'
# http://abd54c1e2d20d41a0858c0f1c452cc28-83569315.ap-northeast-2.elb.amazonaws.com:8080/#scale=1.5

출력된 주소로 접속하면 아래의 이미지와 kube-ops-view 가 설치된 것을 확인할 수 있다.

그 다음 Nginx 를 이용해서 Deployment 로 배포하기 위해 아래의 yaml 파일을 이용했다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80

아래의 이미지는 Nginx 파드 8개를 실행시킨 터미널의 화면이다.

kube-ops-view 에서도 파드가 노드 3개에 걸쳐 총 8개가 생성된 것을 확인할 수 있다.

이제 파드를 50개로 늘려보자.

kubectl scale deployment nginx-deployment --replicas=50

아래의 이미지에서는 각 노드당 14개의 Nginx 파드가 생성되었다.

노드마다 기본적으로 실행하는 3개의 파드가 있기 때문에 하나의 노드에서 실행할 수 있는 파드의 개수는 17-3 = 14가 되는 것이다.

노드 3개에 걸쳐 총 14*3 = 42개의 파드가 생성되었고, 나머지 8개의 파드는 생성되지 못해서 우측에 검은색 상자로 남아있는 것을 확인할 수 있다.

클러스터 트래픽 분산

서비스(Service)

쿠버네티스는 클러스터의 네트워크 트래픽을 제어하기 위해 서비스라는 컴포넌트를 제공한다.

서비스는 파드에서 실행되고 있는 애플리케이션을 네트워크에 노출(expose)하기 위해 사용된다.

서비스는 클러스터 내부의 애플리케이션끼리 통신하거나, 클러스터 외부의 애플리케이션 또는 외부의 사용자와 연결할 수 있도록 돕는다.

서비스가 필요한 이유는 파드의 IP 가 동적으로 생성되고 사라지기 때문에 파드의 IP 만으로는 클러스터 내부와 외부의 통신을 계속 유지하기 어렵다.

따라서 클러스터 내부에서 고정 IP 를 가지도록 하면 파드의 IP 변경 여부에 상관없이 외부에서는 단일한 네트워크 진입점을 갖게 되는 장점이 있다.

서비스의 종류는 크게 4가지이다.

  1. ClusterIP (기본)
  2. NodePort
  3. LoadBalancer
  4. ExternalName

각각의 설명에 대해서는 생략하고, AWS 에서 제공하는 LoadBalancer Controller 에 대해 살펴보자.

로드밸런서 컨트롤러(LoadBalancer Controller)

AWS 에서는 쿠버네티스 클러스터에서 AWS 로드 밸런서(ELB)를 사용할 수 있도록 로드밸런서 컨트롤러(LoadBalancer Controller)라는 플러그인을 제공하고 있다.

쿠버네티스의 인그레스를 이용해서 ALB 를 프로비저닝 할 수도 있고, 서비스를 이용해서 NLB 를 프로비저닝 할 수도 있다.

NLB 를 이용할 때 트래픽을 전달하는 방법은 instance 와 ip 모드 2가지가 있다. (참고링크)

  1. instance
    • 노드의 kube-proxy 가 서비스의 NodePort 를 통해 파드로 트래픽을 전달한다.
  2. ip
    • 파드의 IP 주소로 트래픽을 직접 전달한다. kube-proxy 를 거치지 않기 때문에 패킷 전달 과정에서 생기는 오버헤드를 줄일 수 있는 장점이 있다.
    • 이 모드를 사용하기 위해서는 AWS VPC CNI 플러그인을 사용해야 한다.

이번 실습에서는 NLB IP 모드를 이용해서 할 것이다.

구조를 시각화하면 아래의 이미지와 같다.

출처: 스터디 공유 자료

클러스터 안에서 LoadBalancer Controller 파드가 작동하면서 지속적으로 ELB 에 파드의 IP 를 제공한다.

그래서 외부의 트래픽이 로드밸런서로 들어오면 파드로 바로 트래픽을 전달해준다.

로드밸런서 컨트롤러 배포 실습

AWS 로드밸런서 컨트롤러를 배포해보자.

  1. OIDC 확인

    • AWS 로드밸런서가 EKS 클러스터 자원에 접근할 수 있도록 설정해주기 위해 OIDC 로 인증해야 한다.
    • OIDC 가 존재하는지 확인하기 위해 아래의 명령어를 실행한다.
    aws eks describe-cluster --name $CLUSTER_NAME --query "cluster.identity.oidc.issuer" --output text
    aws iam list-open-id-connect-providers | jq
  2. IAM Policy 생성

    • AWS LoadBalancer Controller 를 설치하기 위해 IAM 정책을 생성한다.
    curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.5.4/docs/install/iam_policy.json
    aws iam create-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json
  3. 생성된 IAM Policy ARN 확인

    aws iam get-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --query 'Policy.Arn'
    # "arn:aws:iam::265524074804:policy/AWSLoadBalancerControllerIAMPolicy"
  4. AWS 로드밸런서 컨트롤러를 위한 서비스 어카운트 생성

    • 현재 접속한 AWS 계정의 IAM 서비스 어카운트를 생성한다.
    • 서비스 어카운트는 쿠버네티스의 인증과 권한을 관리하는 개념이다.
    eksctl create **iamserviceaccount** --cluster=$CLUSTER_NAME --namespace=kube-system --name=**aws-load-balancer-controller** --role-name **AmazonEKSLoadBalancerControllerRole** \
    --attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/**AWSLoadBalancerControllerIAMPolicy** --override-existing-serviceaccounts --**approve**
  5. IRSA 정보 확인

    • EKS 클러스터에서 IAM 서비스 계정을 조회한다.
    eksctl get iamserviceaccount --cluster $CLUSTER_NAME
    #NAMESPACE	  NAME				                  ROLE ARN
    #kube-system	aws-load-balancer-controller	arn:aws:iam::265524074804:role/AmazonEKSLoadBalancerControllerRole
  6. 서비스 어카운트 확인

    kubectl get serviceaccounts -n kube-system **aws-load-balancer-controller** -o yaml | yh
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      annotations:
        eks.amazonaws.com/role-arn: arn:aws:iam::265524074804:role/AmazonEKSLoadBalancerControllerRole
      creationTimestamp: "2024-03-12T05:24:32Z"
      labels:
        app.kubernetes.io/managed-by: eksctl
      name: aws-load-balancer-controller
      namespace: kube-system
      resourceVersion: "22491"
      uid: 606d814a-9e6b-4037-a57a-eff0fd5492c6
  7. helm chart 로 AWS 로드밸런서 컨트롤러 설치

    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
  8. 설치 확인

    kubectl get crd
    #NAME                                         CREATED AT
    #cninodes.vpcresources.k8s.aws                2024-03-12T03:30:21Z
    #eniconfigs.crd.k8s.amazonaws.com             2024-03-12T03:31:28Z
    #ingressclassparams.elbv2.k8s.aws             2024-03-12T05:24:47Z
    #policyendpoints.networking.k8s.aws           2024-03-12T03:30:23Z
    #securitygrouppolicies.vpcresources.k8s.aws   2024-03-12T03:30:21Z
    #targetgroupbindings.elbv2.k8s.aws            2024-03-12T05:24:47Z
    
    kubectl describe deploy -n kube-system aws-load-balancer-controller | grep 'Service Account'
    #  Service Account:  aws-load-balancer-controller

    IAM 역할의 신뢰 관계에 정상적으로 추가된 것을 확인할 수 있다.

이제 NLB 로 서비스와 파드를 배포하는 것을 확인해보자

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

사용한 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

웹 접속 주소를 확인한다.

kubectl get svc svc-nlb-ip-type -o jsonpath={.status.loadBalancer.ingress[0].hostname} | awk '{ print "Pod Web URL = http://"$1 }'
#Pod Web URL = http://k8s-default-svcnlbip-0c7e4641a8-1987934422913f0c.elb.ap-northeast-2.amazonaws.com

접속해보면 아래와 같은 화면이 표시된다.

분산 접속이 되는지 확인한다.

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
#     56 Hostname: deploy-echo-7f579ff9d7-m76tw
#     44 Hostname: deploy-echo-7f579ff9d7-rz4bj

인그레스(Ingress)

인그레스는 ALB 로 작동한다.

인그레스를 이용해서 2048 이라는 게임을 배포해보자.

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

사용한 yaml 파일은 아래와 같다.

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

인그레스가 생성되었는지 확인한다.

kubectl get ingress -n game-2048 ingress-2048 -o jsonpath={.status.loadBalancer.ingress[0].hostname} | awk '{ print "Game URL = http://"$1 }'
#Game URL = http://k8s-game2048-ingress2-70d50ce3fd-2842956.ap-northeast-2.elb.amazonaws.com

표시된 주소로 접속하면 아래와 같은 화면이 표시된다.

AWS EC2 로드 밸런서 항목에서도 ALB 가 프로비저닝 된 것을 확인할 수 있다.

그리고 로드밸런서가 직접 파드의 IP 주소로 트래픽을 분산하는 것을 알 수 있다.

배포한 파드의 IP 주소를 확인해보면 동일한 것을 확인할 수 있다.

kubectl get pod -n game-2048 -owide
#NAME                               READY   STATUS    RESTARTS   AGE     IP             NODE                                               NOMINATED NODE   READINESS GATES
#deployment-2048-75db5866dd-nfzmn   1/1     Running   0          2m10s   192.168.2.9    ip-192-168-2-161.ap-northeast-2.compute.internal   <none>           <none>
#deployment-2048-75db5866dd-tfbbj   1/1     Running   0          2m10s   192.168.1.33   ip-192-168-1-78.ap-northeast-2.compute.internal    <none>           <none>

후기

네트워크는 정말 배울 것도 많고 복잡하다는 걸 느꼈다.

CNI 에서 나오는 BGP 개념이 제대로 이해되지 않았지만, 다른 내용도 많다보니 깊이 다룰 수 없었다.

그래도 다행이었던 점은 이전에 if(kakao) 2022 에서 김상영 님께서 발표한 도커 없이 컨테이너 만들기를 따라해본 덕분에 이번 스터디의 내용이 잘 이해되었다.

전반적으로 쿠버네티스의 개념들이 모호했었는데, 이번 주 스터디 내용을 학습하면서 조금 더 개념들이 명확하게 이해되고 있다.

공부할 내용이 정말 방대하지만, 한 번에 다 이해하기보다 반복해서 학습하다보면 금방 익숙해질 것 같다.

오랜만에 서브넷 마스크와 CIDR 표기법에 대해서도 다시 복습할 수 있어서 좋았다.

참고자료

profile
성장하는 개발자, 한준혁입니다.

0개의 댓글