본문 바로가기
클라우드

Kubernetes NodePort vs LoadBalancer vs Ingress 비교

by 내기록 2022. 11. 24.
반응형

목차 LIST

     

     

    시작하기 전에 : ClusterIP - 쿠버네티스 내부에서만 Pod에 접근하기

    appVersion: v1
    kind: Service
    metadata:
      name: hostname-svc-clusterip
    spec:
      ports:
        - name: web-port
          port: 8080 # 쿠버 내부에서만 사용가능한 고유한 IP(Cluster IP)에 접근할 때 사용할 Port
          targetPort: 80 # 접근 대상 Pod들이 내부적으로 사용하고 있는 port를 입력해야 합니다.
      selector:
        app: webserver # 이 서비스가 어떤 라벨을 가지는 pod에 접근 가능한지
      type: clusterIP

     

    ClusterIP 서비스 생성 결과

    $ kubectl get svc
    NAME                     TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
    hostname-svc-clusterip   ClusterIP   10.101.98.33   <none>        8080/TCP   10s

    이 서비스를 사용해 pod에 접근하는 방법은 cluster-ip와 port로 요청을 보내는 것입니다. CLUSTER-IP는 쿠버네티스 내에서만 사용 가능한 내부 IP로 이 IP를 사용하여 서비스에 연결된 pod에 접근할 수 있습니다.

    # curl 10.101.98.33:8080 --silent

     

    이렇게 요청하면 서비스와 연결된 여러 pod에 자동으로 요청이 분산됩니다. 즉, 서비스를 생성할 때 별도 설정을 하지 않아도 연결된 pod에 대해 로드 밸런싱을 수행합니다.

     

    서비스는 서비스 이름으로도 접근 가능합니다. 쿠버네티스는 내부 DNS를 가지며 pod들은 자동으로 이 DNS를 사용하게 설정되기 때문입니다. 실제로 pod가 클러스터 내에서 다른 pod로 연결할 때는 서비스 이름같은 도메인 이름을 사용하는 것이 일반적입니다. (pod와 연결된 서비스 이름을 사용해서 접근합니다.)

     

    # curl hostname-svc-clusterip:8080 --silent

     

     

    구조를 간단히 살펴보면 위와 같습니다. 쿠버네티스 클러스터 내부에서 10.101.98.33:8080 또는 hostname-svc-clusterip:8080으로 요청으로 pod에 접근이 가능합니다.

     

    ClusterIP는 외부에서는 접근할 수 없다는 점에 유의해야 합니다! 외부에 노출해야 할 때 사용하는 것이 앞으로 설명할 NodePort, LoadBalancer 타입입니다.

     

    (참고)

    서비스의 label selector와 pod의 label이 매칭되면 쿠버네티스는 자동으로 endpoint 오브젝트를 별도로 생성합니다.

    예를 들면 위에서 생성한 서비스의 엔드포인트는 아래와 같습니다.

     

    $ kubectl get endpoint
    NAME                      ENDPOINTS                                           AGE
    hostname-svc-clusterip    192.168.1.109:80,192.168.2.86:80,192.168.3.89:80    10m

     

     

    NodePort - 서비스를 이용해 포트를 외부에 노출하기

    https://medium.com/google-cloud/kubernetes-nodeport-vs-loadbalancer-vs-ingress-when-should-i-use-what-922f010849e0

     

    ClusterIP 타입의 서비스는 내부에서만 접근 가능하지만, NodePort 타입의 서비스는 클러스터 외부에서도 접근할 수 있습니다. 모든 노드의 특정 포트를 개방해 서비스에 접근하는 방식입니다. (스웜 모드에서 컨테이너를 외부로 노출하는 방식과 비슷함)

     

    apiVersion: v1
    kind: Service
    metadata:
      name: my-service
    spec:
      type: NodePort
      selector:
        app.kubernetes.io/name: MyApp
      ports:
          # 기본적으로 `targetPort` 는 `port` 필드와 동일한 값으로 설정된다.
        - port: 80
          targetPort: 80
          # 선택적 필드
          # 기본적으로 그리고 편의상 쿠버네티스 컨트롤 플레인은 포트 범위에서 할당한다(기본값: 30000-32767)
          nodePort: 30007

    위 예제를 사용하면 내부 IP 또는 외부 IP를 통해 30007번 포트로 접근하면 동일한 서비스에 연결할 수 있습니다.

     

    ** GKE에서 쿠버네티스를 사용하는 경우 각 노드의 랜덤한 포트에 접근하기 위해 별도로 방화벽 설정을 추가해야 합니다. AWS에서도 마찬가지로 Security Group의 별도의 Inbound 규칙을 추가해야 합니다.

    $ gcloud compute firewall-rules create sun-nodeport-svc --allow=tcp:30007 # 규칙 추가
    $ gcloud compute firewall-rules delete sun-nodeport-svc # 규칙 삭제

     

    참고로, Nodeport 서비스는 ClusterIP의 기능을 포함하고 있기 때문에 쿠버네티스 클러스터 서비스의 내부 IP와 DNS 이름을 사용해 접근할 수 있습니다. 즉, NodePort 타입의 서비스는 내부/외부 네트워크 양쪽에서 접근할 수 있습니다.

     

    기본적으로 NodePort가 사용할 수 있는 포트 범위는 30000~32768이지만, API 서버 컴포넌트의 실행 옵션을 변경해서 포트 범위를 지정할 수 있습니다.
    --service-node-port-range=30000-35000
    너무 낮은 포트 번호는 시스템에 의해 예약된 포트일 수 있기 때문에 가능하면 기본으로 설정된 30000번 이상의 포트를 사용하는 것이 좋습니다.

    실제 운영 환경에서 NodePort로 서비스를 외부에 제공하는 경우는 많지 않습니다. SSL 인증서 적용 라우팅과 같은 복잡한 설정을 서비스에 적용하는 것은 어렵기 때문입니다. 따라서 인그레스(Ingress)에서 간접적으로 사용되는 경우가 많습니다. Ingress는 아래에서 설명할 예정이며 아래에서 설명할 LoadBalancer와 NodePort를 합치면 Ingress 오브젝트를 사용할 수 있습니다.

     

    LoadBalancer

     

    https://medium.com/google-cloud/kubernetes-nodeport-vs-loadbalancer-vs-ingress-when-should-i-use-what-922f010849e0

    LoadBalancer 타입의 서비스는 서비스를 생성함과 동시에 로드 밸런서를 새롭게 생성해 pod와 연결합니다. Nodeport는 각 노드의 IP를 알아야 pod에 접근할 수 있지만, LoadBalancer 타입의 서비스는 클라우드 플랫폼으로부터 도메인 이름과 IP를 할당받기 때문에 Nodeport보다 쉽게 pod에 접근할 수 있습니다.

     

    ** 단, 로드 밸런서를 동적으로 생성하는 기능을 제공하는 환경에서만 사용할 수 있습니다. AWS, GCP 등과 같은 클라우드 플랫폼 환경에서만 LoadBalancer 타입을 사용할 수 있으며, 가상 환경이나 온프레미스 환경에서는 사용이 어려울 수 있습니다.

    온프레미스 환경에서 LoadBalancer 타입의 서비스를 사용하기 위해서는 MetalLB나 오픈스택과 같은 특수한 환경을 직접 구축해야 합니다. 그 중에서 MetalLB라는 오픈소스 프로젝트를 사용하는 것을 책에서는 추천하고 있습니다.

     

    ** 책에는 아래와 같이 되어있으나, 공식 홈페이지에는 IngressIP와 함께 사용하는 것으로 나와있습니다.

    appVersion: v1
    kind: Service
    metadata:
      name: hostname-svc-lb
    spec:
      ports:
        - name: web-port
          port: 80
          targetPort: 80
      selector:
        app: webserver
      type: LoadBalancer

     

    ** 공식 홈페이지 예시

    loadBalancer IP 필드가 지정되지 않으면 임시 IP 주소로 lb가 설정된다. ib ip를 지정했지만 클라우드 공급자가 이 기능을 지원하지 않는 경우, 설정한 lb ip 피드는 무시됩니다.

    apiVersion: v1
    kind: Service
    metadata:
      name: my-service
    spec:
      selector:
        app.kubernetes.io/name: MyApp
      ports:
        - protocol: TCP
          port: 80
          targetPort: 9376
      clusterIP: 10.0.171.239
      type: LoadBalancer
    status:
      loadBalancer:
        ingress:
        - ip: 192.0.2.127

     

    <결과>

    https://www.middlewareinventory.com/blog/internal-external-load-balancer-aws-eks/

    LB 타입은 NodePort나 ClusterIP와 동일하게 서비스의 IP(Cluter-ip)가 할당되며, pod에서는 서비스의 ip나 서비스 명으로 서비스 접근이 가능합니다. 

    여기서 눈여겨봐야 하는 것은 'EXTERNAL-IP'이다. 이 주소는 클라우드 플랫폼이 자동으로 할당하는 것이며, 이 주소와 80포트(위 yaml 파일의 ports.port)를 통해 pod에 접근할 수 있습니다.

     

    - 그렇다면 위 이미지에서 30767 port는 무엇을 의미하는 것일까? (80:30767/TCP)

    각 node에서 동일하게 접근할 수 있는 port 번호를 의미한다. 30767 포트를 통해 각 node의 ip로 접근하면 로드 밸런서와 동일하게 pod에 접근할 수 있습니다.

     

    - LB를 사용한 접근 : ab93d..us-east-2..amazonaws.com:80

    - 30767를 사용한 접근 : 10.43.0.30:30767, 10.43.0.31:30767, 10.43.0.32:30767 (여기서 10.43.3x는 node의 ip이다)

     

    참고: 시작하세요! 도커/쿠버네티스

    위 그림을 보면, LB 타입의 서비스가 생성됨과 동시에 모든 워커노드는 pod에 접근할 수 있는 랜덤한 port를 개방합니다. (30767)

    LB로 요청이 들어오면 쿠버네티스의 워커 노드 중 하나로 전달되며, 이때 개방된 30767 포트를 사용합니다.

     

     

    * 트래픽의 분배를 결정하는 서비스 속성 : externalTrafficPolicy

    LB와 NodePort에 모두 해당하는 문제입니다. 

    외부 트래픽 정책(ExternalTrafficPolicy)이란 외부 트래픽에 대한 응답으로 Service가 노드 안(Local)에서만 응답할지 Cluster 전체(Cluster)에서 응답할지 결정하는 옵션입니다.

    Local 타입은 요청한 클라이언트 IP를 유지하고 네트워크 hop이 길어지지 않게 막아주지만 잠재적으로 트래픽 분산에 대한 불균형을 가져올 수 있습니다. 반면에, Cluster 타입은 클라이언트IP을 보존하지 않고 네트워크 hop을 길게 만들지만 전체적으로 부하가 분산될 수 있도록 합니다.

     

     

    <Cluster 타입>

    https://ihp001.tistory.com/240 시작하세요! 도커/쿠버네티스

     

    위 그림으로 예를 들면, 모든 노드에서 310000번 포트가 개방되었고, 워커 노드 A,B에 pod가 각각 생성되어 있다고 가정했을 때, 워커노드 A로 들어오는 요청은 pod a 또는 pod b로 전달될 것입니다.

    A노드로 들어오는 요청을 pod a에서 처리해도 되는데, pod b로 전달되면 불필요한 네트워크 홉(hop)이 한 단계 더 발생하게 됩니다.

    또한 노드 간의 리다이렉트가 발생하여 트래픽의 출발지 주소가 바뀌는 SNAT이 발생하여 클라이언트의 IP 주소가 보존되지 않는다는 단점이 있습니다.

     

    ** Traffic Policy: Cluster

     

     service yaml파일의 externalTrafficPolicy 설정으로 변경이 가능합니다. default는 Cluster로 설정되어 있습니다.

    이 값을 Local로 설정하면 pod가 생성된 노드에서만 pod로 접근 가능하여 로컬에 위치한 pod 중 하나로 요청이 전달됩니다. 즉, 추가적인 네트워크 홉이 발생하지 않으며, 전달되는 요청의 클라이언트 IP 또한 보존됩니다.

     

    ** Traffic Policy: Local

    kube-proxy는 전달 받을 pod가 있는 노드에만 NodePort를 오픈합니다. 

    이 옵션을 사용하면 pod가 있는 노드에만 포트를 열고 그 외에는 트래픽을 전달하지 않고 drop 시키기 때문에 client의 IP가 보존될 수 있습니다. (다른 노드로 네트워크 hop이 발생하지 않기 때문)

     

     

    https://coffeewhale.com/packet-network3

    위에 있는 Cluster 그림과 비교하면, Local은 pod가 있는 Node1,2에서만 요청을 받을 수 있으며 받은 요청은 Node 내의 pod에서 바로 처리하며 다른 Node로 보내지 않는 것을 확인할 수 있습니다.

     

     

    https://ihp001.tistory.com/240 시작하세요! 도커/쿠버네티스

    Local로 설정하면 위와 같이 설정됩니다. 하지만 Local로 설정하는게 항상 좋은 것은 아닙니다. 각 node에 pod가 고르지 않게 스케줄링 됐을 때, 요청이 고르지 않게 분산될 수 있기 때문입니다.

    아래 그림으로 예를 들겠습니다.

    https://ihp001.tistory.com/240 시작하세요! 도커/쿠버네티스

    특정 노드의 pod에 부하가 집중될 수 있으므로 자원 활용률(utilization) 측면에서 바람직하지 않을 수 있다는 것을 의미합니다.

     

    결론 : Cluster, Local은 둘 다 장단이 있습니다. 불필요한 네트워크 홉으로 인한 레이턴시나 클라이언트의 IP 보존이 중요하지 않다면 Cluster를 사용해도 되지만 반대라면 Local을 사용하는 것이 좋은 선택일 수 있습니다.

     

     

    Ingress

    위에서 설명한 LoadBalancer와 NodePort를 합치면 인그레스 오브젝트를 사용할 수 있습니다.

     

    인그레스를 사용하는 이유

    # Ingress를 사용하지 않는 경우

    도서 시작하세요! 도커/쿠버네티스

    각 디플로이먼트에 대응하는 서비스를 하나씩 연결해 주어야 합니다. 위 방식으로 했을 때 서비스마다 세부적인 설정이 필요한 경우 복잡성이 발생합니다. SSL/TLS 보안 연결, 접근 도메인 등을 구현하려면 각 서비스와 디플로이먼트에 대해 각각 설정을 해야 하기 때문입니다.

    Nodeport나 LoadBalancer 타입의 서비스로도 가능하지만 Ingress 오브젝트를 사용하면 URL 엔드포인트를 단 하나만 생성함으로써 이러한 번거로움을 쉽게 해결할 수 있습니다. 위 디플로이먼트를 외부로 노출하는 인그레스를 생성하면 아래와 같습니다.

     

    # Ingress를 사용하는 경우

    https://medium.com/google-cloud/kubernetes-nodeport-vs-loadbalancer-vs-ingress-when-should-i-use-what-922f010849e0

    3개의 서비스에 대해 각각의 URL이 존재하는 대신 Ingress에 접근하기 위한 하나의 URL만 존재합니다.

    요청은 Ingress에서 정의한 규칙에 따라 처리되어 적절한 디플로이먼트의 pod로 전달됩니다. 이 과정에서 라우팅 정의나 보안 연결과 같은 세부 설정은 각각의 디플로이먼트에 대해 적용할 필요 없이 인그레스에서 처리 규칙을 정의하기만 하면 됩니다. -> 외부 요청에 대한 처리 규칙을 쿠버네티스 자체 기능으로 편리하게 관리할 수 있다는 것이 핵심입니다.

     

    - 공식 홈페이지 예시

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: ingress-wildcard-host
    spec:
      rules:
      - host: "foo.bar.com"
        http:
          paths:
          - pathType: Prefix
            path: "/bar"
            backend:
              service:
                name: service1
                port:
                  number: 80
      - host: "*.foo.com"
        http:
          paths:
          - pathType: Prefix
            path: "/foo"
            backend:
              service:
                name: service2
                port:
                  number: 80

    YAML 파일의 몇 가지 설정을 살펴보면 아래와 같습니다.

    (1) host : 해당 도메인 이름으로 접근하는 요청에 대해 처리 규칙을 적용합니다. 여러 host를 정의할 수 있습니다.

    (2) path : 해당 경로로 들어온 요청을 어느 서비스로 전달할지 정의합니다. 여러 개의 path를 정의해 경로를 처리할 수 있습니다.

    (3) service.name, service.port : path로 들어온 요청이 전달될 서비스와 포트입니다. 위에서는 /foo 경로로 들어온 요청을 service2 서비스의 80 포트로 전달합니다.

     

    위와 같이 Ingress를 생성했을 때, Ingress는 요청을 처리하는 규칙을 정의하는 오브젝트일 뿐 외부 요청을 받아들일 수 있는 실제 서버가 아니기 때문에 인그레스 컨트롤러(Ingress Controller)라는 특수 서버에 적용해야 규칙을 사용할 수 있습니다.

    즉, 실제로 요청을 받아들이는 것은 인그레스 컨트롤러 서버이며, 이 서버에서 인그레스 규칙을 로드해 사용합니다.

     

    인그레스 컨트롤러(Ingress Controller)

    인그레스 컨트롤러는 여러 종류가 있지만 대표적으로 쿠버네티스에서 공식적으로 개발하고 있는 Nginx 웹 서버 인그레스 컨트롤러를 활발히 사용합니다. 이 외에도 Kong이라는 API 게이트웨이나 GKE 등의 클라우드 클랫폼에서 제공하는 여러 인그레스 컨트롤러가 있습니다.

     

    Nginx 웹 서버 인그레스 컨트롤러는 쿠버네티스에서 공식적으로 개발하고 있기 때문에 설치에 필요한 YAML파일을 공식 깃허브에서 직접 내려받을 수 있습니다. (https://github.com/kubernetes/ingress-nginx)

    Nginx 인그레스 컨트롤러를 모두 구성하면 자동으로 LoadBalancer타입의 서비스가 생성되며, external-ip를 통해 외부에서 접속이 가능합니다.

     

    https://www.ibm.com/cloud/blog/kubernetes-ingress

    Ingress Controller에서 요청을 받고, 인그레스 규칙을 적용해서 서비스를 나누는 것으로 진행됩니다. 따라서 ingress controller 서비스의 external-ip를 ingress yaml의 host에서 받을 수 있게 설정해야 정상적으로 인그레스 규칙이 적용됩니다.

     

    예를 들어, Ingress Controller의 external-ip는 a202255.ap-northeast-2.elb.amazonaws.com인데, ingress 규칙은 위 예시처럼 설정되어 있다면 

      - host: "foo.bar.com"

    ingress 규칙이 적용되지 않습니다. 따라서 ingress yaml파일의 host를 Ingress controller의 external-ip 를 받을 수 있게 설정하여 진행합니다.

      - host: "a202255.ap-northeast-2.elb.amazonaws.com"

    참고로, ingress yaml 파일의 host, path 항목 등은 필수 정의항목이 아니며 여기서는 테스트를 위해 설정했습니다.

     

     

    인그레스 컨트롤러의 동작 원리 이해

    https://ihp001.tistory.com/245 시작하세요! 도커/쿠버네티스 도서

    위에서 설명한 인그레스 컨트롤러의 동작 과정을 살펴보면, 먼저 인그레스 컨트롤러의 서비스인 LB로 요청이 들어옵니다.

    위 예시를 기준으로 보면 host는 a202255.ap-northeast-2.elb.amazonaws.com 입니다.

    인그레스 컨트롤러에는 ingress yaml 파일로 규칙이 적용되어 있으며, 들어온 요청을 규칙에 따라 적절한 서비스로 전달합니다.

     

    참고로, 인그레스를 생성하면 인그레스 컨트롤러는 자동으로 인그레스를 로드해 Nginx 웹 서버에 적용합니다. 기본적으로 모든 네임스페이스의 인그레스 리소스를 읽어와 규칙을 적용합니다.

    참고) ingress를 여러 개 사용하는 경우 yaml파일에 annotations를 사용하여 ingress 규칙을 적용할 인그레스 컨트롤러를 설정할 수 있습니다.

     

    바이패스(bypass)

      - host: "*.foo.com"
        http:
          paths:
          - pathType: Prefix
            path: "/foo"
            backend:
              service:
                name: service2
                port:
                  number: 80

    위 인그레스 규칙에 맞는 요청이 들어오면 service2의 80포트로 요청을 전달합니다. 하지만 요청이 실제로 service2라는 서비스로 전달되는 것은 아니며, 인그레스 컨트롤러는 서비스에 의해 생성된 엔드포인트로 요청을 직접 전달합니다. 즉, 서비스의 ClusterIP가 아닌 엔드포인트의 실제 종착 지점들로 요청이 전달되는 것인데 이러한 동작을 바이패스라고 부릅니다. 서비스를 거치지 않고 포트로 직접 요청이 전달되기 때문입니다.

     

    참고) 엔드포인트의 실제 종착 지점은 무엇인가?

    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-container
            image: nginx
            ports:
            - containerPort: 80
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: service2
    spec:
      type: NodePort
      ports:
      - name: http
        port: 80
        targetPort: 80
        nodePort: 30001 
        protocol: TCP
      selector:
        app: nginx

    위 매니페스트를 배포한 결과는 아래와 같습니다.

    $ kubectl get endpoints
    NAME       ENDPOINTS                      AGE
    service2   10.244.3.4:80,10.244.2.5:80    20h
    $ kubectl get pods -o wide
    
    NAME                                READY     STATUS    RESTARTS   AGE      IP            NODE
    nginx-deployment-3800858182-jr4a2   1/1       Running   0          1d       10.244.3.4    kube-node2
    nginx-deployment-3800858182-kna2y   1/1       Running   0          1d       10.244.2.5    kube-node2

    결론: 실제 종착 지점이라는 것은 pod의 ip로 바이패스는 서비스를 거치지 않고 생성된 엔드포인트로 직접 요청을 하는 것을 의미합니다.

     

     

     

     

    References

    https://kubernetes.io/ko/docs/concepts/services-networking/service/

    시작하세요! 도커/쿠버네티스 - 용찬호

    https://coffeewhale.com/packet-network3

    https://ozt88.tistory.com/65

     

    반응형

    댓글