[Kubernetes] # 쿠버네티스 네트워크 구성도

empty·2020년 12월 20일
19

Kubernetes

목록 보기
2/7
post-thumbnail

❗️ 본 포스팅은 개인적으로 공부하고 이해한 내용을 작성한 게시글입니다. 잘못된 내용이 있을 수 있습니다.

쿠버네티스는 기본적으로 docker의 네트워크 구성도를 베이스로 한다. docker와 비슷하지만 미묘한 차이가 있다. 따라서 파드 네트워킹을 이해하려면 도커 컨테이너의 네트워킹을 간단하게 이해하고 파드의 네트워킹을 알아보자.

도커의 컨테이너 네트워킹

아래의 그림은 도커의 브릿지 타입 네트워크 구조이다.

위 그림의 docker0호스트 네트워크 네임스페이스 또는 디폴트 네트워크 네임스페이스 라고 하고 아래의 veth0, veth1 (베스) 들은 컨테이너 네트워크 네임스페이스 라고 한다.

네트워크 네임스페이스는 별도의 ARP, 라우팅, iptables가 존재한다.

호스트의 기본 네트워크는 docker0 에서 만들어지고 관리되고 컨테이너의 기본 네트워크는 veth0, veth1에서 만들어지 관리된다.

네트워크 네임 스페이스는 서로 연결되기 전까지는 독립적으로 동작한다. 이 때 서로의 네트워크 네임스페이스를 연결시켜주는 역할을 하는 것이 veth(Virtual Ethernet)라는 가상장치이다.

이렇게 베스로 양쪽 끝을 서로 다른 네트워크 네임스페이스 또는 장치에 연결할 수 있으므로 베스쌍(veth pair)이라고도 한다.

쿠버네티스의 파드 네트워킹


쿠버네티스는 도커와는 달리 파드단위로 컨테이너들을 관리한다. 파드는 여러개의 컨테이너로 구성 될 수 있는데 컨테이너들은 모두 동일한 IP를 부여받을 수 있다. 이 때 동일한 IP를 부여받게끔 해주는 것이 바로 pause 라는 컨테이너 덕택이다.

하나의 파드를 생성하게 되면 그 파드의 컨테이너들은 pause 컨테이너의 네트워크 네임스페이스의 인터페이스 IP 하나를 갖게되는 것이다. 따라서 컨테이너들끼리 127.0.0.1로 포트를 구분하여 서로 통신할 수 도 있다.

pause 컨테이너는 파드가 실행되고 있는 노드에서 docker ps 명령어로 확인 할 수 있다.

쿠버네티스의 네트워크 방식을 간단하게 살펴보았다. 이제 쿠버네티스에서 발생할 수 있는 모든 네트워킹의 방식에 대해서 알아보자

#1. 싱글 노드에서의 Pod to Pod 통신


결론 : 네트워크 인터페이스 (= kubenet 혹은 CNI)를 통하여 파드에 할당되어 있는 고유한 IP주소로 통신할 수 있다.

기본적으로 쿠버네티스는 kubenet이라는 아주 기본적이고 간단한 네트워크 플러그인을 제공해준다. 하지만 이 네트워크 플러그인은 크로스 노드 네트워킹이나 네트워크 정책설정과 같은 고급 기능은 구현이 되어있지 않다. 따라서 CNI(Container Network Interface)의 스펙을 준수하는 네트워크 플러그인을 따로 사용해야한다.

VM에 쿠버네티스 구성 후 CNI를 설치하는 이유이다.

각 파드는 고유한 IP주소를 가진다. 따라서 각 파드는 네트워크 인터페이스 (kubenet 혹은 CNI)를 통하여 고유한 IP 주소로 통신할 수 있다.

#2. 멀티 노드에서의 Pod to Pod 통신


결론 : 오버레이 네트워크 기능을 구현하기 위해 CNI를 설치하고 라우터를 경유하여 통신하게 된다.

Pod → veth → CNI(Flannel) → eth0라우터(=VPC)eth1 → CNI(Flannel) → veth → Pod

여기서 오버레이 네트워크 기능에는 가상 네트워크 인터페이스, 브릿지 네트워크, 라우터의 라우팅 Rule의 조합을 일컬어 오버레이 네트워크라고 한다.

CNI의 필요성

아래의 그림처럼 기존의 도커 네트워크 구조로 여러 노드를 구성하게 되면 해당 노드에서 생성된 도커 브릿지(docker0)의 IP는 두 노드 둘 다 172.17.0.1의 IP를 갖게 될 것이다. 이 경우 해당 IP로 요청이 왔을 때 노드1과 노드2 중 어떤 노드의 파드로 패킷이 가야하는지 알 수 없다.

이런 문제를 해결하기 위해 쿠버네티스에서는 CNI라는 네트워크 인터페이스를 추가적으로 설치한다. CNI는 호스트 네트워크 인터페이스의 각종 네트워크 기능들(iptables, 커널 라우팅, 터널링, 브릿지)을 사용하게 해준다.

즉, CNI가 커널 라우팅 기능동적 라우팅 기능으로 외부 라우터에 각 호스트 사이 라우팅을 정의해둔것이다!

이런 CNI의 역할 덕분에 라우터의 라우팅 테이블에 노드 끼리의 Rule이 추가가 되고 이를 통해서 서로간의 파드끼리 통신할 수 있게 된다. 다시한번 CNI가 무엇이고 어떤 역할을 해주는지 정리해보자.

CNI란?

Container Network Interface의 약어로 쿠버네티스 네트워크에 오버레이 네트워크를 구성해주고 파드와 호스트 인터페이스를 연결해주는 부분을 별도의 모듈로 분리한 플러그인이다.

CNI의 기능

  • 커널 라우팅
  • 동적 라우팅
  • Pod 인터페이스 생성 및 IP, Subnet, Routing Table 설정
  • Proxy ARP 기능

#3. Pod To Service


결론: kube-proxy의 netfilter에 정의되어있는 chain rule에 의하여 요청을 포워딩한다.

해당 네트워크 방식을 이해하기 전에 Service란 리소스에 대해서 알아보고 네트워크 구조와 동작방식에 대해서도 알아보자.

쿠버네티스는 기본적으로 Pod는 쉽게 대체될 수 있는 존재이기 때문에 IP로 Service와 통신하기에는 부적절하다. 따라서 Pod앞단에 Reverse Proxy를 위치시키는 방법이있다. 이 Reverse Proxy를 수행해주는 추상적인 리소스가 Service이다. Service는 쿠버네티스의 리소스타입 중 하나로써 각 Pod로 트래픽을 포워딩해주는 프록시 역할을한다. Pod 네트워크와 동일하게 Service 네트워크 또한 가상 IP주소이다.

즉, 쿠버네티스 클러스터 내부에서 통신할 때는 이 Service의 IP를 거쳐서 통신하게 된다.

위의 그림처럼 두 개의 워커노드가 있고 하나의 게이트웨이를 통해 서로 연결되어 있다. 또한 마스터 노드에서 아래의 명세를 통해 service 리소스를 생성했다고 가정한다.

apiVersion: v1
kind: Service
metadata:
  name: was-svc # service의 이름
spec:
  selector:
    app: was # was Pod의 라벨
  type: ClusterIP # service의 type 설정
  ports:
    - protocol: TCP
      port: 80 # service에서 서버 컨테이너 어플리케이션과 매핑시킬 포트 번호
      targetPort: 8080 # 서버 컨테이너에서 구동되고 있는 서버 어플리케이션 포트 번호

$ kubectl create -f svc-default.yml

위의 그림에서 Web Pod가 WAS의 자원을 요청받는 과정을 요약해보면 다음과 같다.

  1. Web Pod가 was-svc에 자원을 요청
  2. CoreDNS가 해당 서비스 이름(was-svc)을 service의 IP(10.3.241.152)로 매핑시켜줌
  3. Web Pod는 Service로 (10.3.241.152:80)으로 패킷요청
  4. veth1는 해당 서비스의 IP를 모름. 따라서 상위 게이트웨이로 패킷전달
  5. cni0도 해당 서비스의 IP를 모름. 따라서 상위 게이트웨이로 패킷을 전달
  6. eth0도 해당 서비스의 IP를 모름. 따라서 상위 게이트웨이로 패킷을 전달해야 하지만..
    • 여기서 kube-proxy의 netfilter에 정의되어 있는 Chain Rule에 의하여 요청을 포워딩하게됨.
  7. 라우터에 정의되어 있는 라우팅 테이블에 의해 Worker Node2로 포워딩
  8. eth1의 kube-proxy에서 netfilter가 패킷을 Listening 중 이고 패킷이 들어오면 cni1로 포워딩 해줌
  9. cni1은 단순 브릿지 네트워크이므로 하위 계층으로 패킷을 넘김
  10. 패킷은 server-svc에 도달하고 매핑된 Pod에 패킷을 넘긴다.

IP는 (Layer 3)는 기본적으로 자신의 Host에서 목적지를 찾지 못하면 상위 게이트웨이로 패킷을 전달한다. 위의 예시에서 보면(5번) client pod는 10.3.241.152 의 위치를 모르기 때문에 보통이라면 최상위에 존재하는 게이트웨이로 전달될 것이지만. kube-proxy라는 컴포넌트를 통해 패킷흐름이 제어가 된다.

kube-proxy는 iptables를 이용하여 chain rule 규칙을 지정하고 패킷을 프록싱하도록 네트워크를 설정한다.

serviceIP를 발견하고 그것을 실제 Pod로 전달하는 것은 모두 netfilter가 담당하게 되었고 kube-proxy는 단순히 netfilter의 규칙을 알맞게 수정하는것을 담당할 뿐이다.

Pod를 생성하게 되면 kube-proxy에서 동적으로 netfilter에 chain rule규칙을 생성한다. 이 규칙은 파드가 생성된 노드에 ssh로 접속 후 iptables -t nat -L 명령어로 확인가능하다.

#4. External To Service


service 네트워크는 Service가 할당받는 네트워크 인터페이스이다.
모든 service 는 Cluster-IP라는 IP 주소를 부여받고 클러스터 내부적으로 이 IP주소를 통해 자신이 포워딩 해야 할 Pod들에게 트래픽을 전달한다.
즉, 기본적으로 service는 클러스터 내부적으로만 통신할 수 있게끔 설계 되어 있다. 그렇다면 외부와 통신은 어떻게 하는것인가?

service는 여러가지 타입을 통해 외부통신을 가능하게끔 기능을 제공한다.

NodePort


NodePort타입의 서비스를 생성하게 되면 kube-proxy가 모든 노드의 eth0 네트워크 인터페이스에 30000-32767 포트 사이의 임의의 포트를 할당한다. 할당된 포트로 요청이 들어오게 되면 이것을 매핑된 ClusterIP(=service)로 전달한다.

NodePort를 생성하면 쿠버네티스 내부적으로 아래의 변경사항이 발생한다.

  • 모든 VM에 30000-32767범위의 임의 포트할당
  • selector에 맞는 Pod들에 대한 ClusterIP 타입의 service 생성
  • kube-proxy가 이를 포워딩 하도록 netfilter의 chain rule을 수정

LoadBalancer


로드밸런서는 기본적으로 외부 클라우드 서비스를 사용하여 로드밸런서를 프로비저닝 할 수 있는 경우에만 사용할 수 있는 서비스 타입이다.

만약 베어메탈 환경에서 로드밸런서를 사용을 하고싶다면 metal-lb라는 플러그인을 따로 설치를 해줘야 사용을 할 수 있게된다.

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376
  clusterIP: 10.0.171.239
  type: LoadBalancer
status:
  loadBalancer: # 여기에 로드밸런서에 대한 내용 추가
    ingress:
    - ip: 192.0.2.127

Ingress



로드밸런서 서비스 타입은 TLS termination 설정이 불가능하고 URL-Path 라우팅이 불가능하다. 그렇기 때문에 한개의 로드밸런서를 이용하여 여러 서비스에 연결하는 것은 불가능하다.

반면 Ingress 서비스 타입은 리버스 프록시를 통해서 클러스터 내부의 Service로 어떻게 포워딩 시킬 것인지 명시한 리소스이다. 로드밸런서와는 달리 TLS termination이나 URL-Path 라우팅을 가능하게 한다.

Ingress는 리소스 타입과 그 리소스 타입을 관리하는 Ingress-Controller가 존재한다. 아래의 코드가 Ingress 리소스 예시이다.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    kubernetes.io/ingress.class: "gce"
spec:
  tls:
    - secretName: my-ssl-secret
  rules:
  - host: testhost.com
    http:
      paths:
      - path: /*
        backend:
          serviceName: service-test
          servicePort: 80

Ingress-Controller는 요청을 적절한 서비스로 전달해야 하는 역할을 담당한다. Ingress를 사용할 때, 요청 받을 서비스를 NodePort 타입으로 설정을 하고 Ingress-controller로 하여금 어떻게 요청을 각 노드에 전달할지 파악하게 한다. 각 클라우드 플랫폼 마다의 Ingress-controller 구현체가 있다.

  • GCP의 Cloud Load Balancer
  • AWS의 Elastic Load Balancer
  • 오픈소스로는 nginx, Haproxy 등..

Reference


0개의 댓글