쌩로그

[Kubernetes] 쿠버네티스 서비스(Service) 본문

Deploy/Kubernetes

[Kubernetes] 쿠버네티스 서비스(Service)

.쌩수. 2025. 3. 5. 16:52
반응형

목록

  1. 포스팅 개요
  2. 본론
      2-1. 서비스(Service)의 종류
      2-2. ClusterIP 타입의 서비스 - 쿠버네티스 내부에서만 파드에 접근하기
      2-3. NodePort 타입의 서비스 - 서비스를 이용해 파드를 외부에 노출하기
      2-4. 클라우드 플랫폼의 로드 밸런서와 연동하기 - LoadBalancer 타입의 서비스
      2-5. 트래픽의 분배를 결정하는 서비스 속성 - externalTrafficPolicy
      2-6. 요청을 외부로 리다이렉트하는 서비스 - ExternalName
  3. 요약

1. 포스팅 개요

이 포스팅은 위키북스 출판사의 '시작하세요! 도커/쿠버네티스'의 제 6장 쿠버네티스 시작하기를 학습하며 기록한 포스팅이다.

그 중 서비스, 파드를 연결하고 외부에 노출(Service)에 대한 내용이다.

2. 본론

지금까지 쿠버네티스에서 컨테이너를 구성하는 가장 중요한 요소인 파드 ,레플리카셋, 디플로이먼트 에 대해서 알아봤다.

그러나 디플로이먼트를 통해 생성된 파드에는 어떻게 접근할 수 있을지에 대해서는 아직 알아보지 않았다.

이전에는 kubectl describe 명령어로 파드의 내부 IP를 직접 확인한 뒤 파드로 직접 접근할 수는 있었지만, 이 방법은 로컬 개발 환경 또는 쿠버네티스 클러스터 내부에서만 사용할 수 있었다.

게다가 도커 컨테이너와 마찬가지로 파드의 IP는 영속적이지 않아 항상 변할 수 있다는 점도 유의해야 한다.
여러 개의 디플로이먼트를 하나의 완벽한 애플리케이션으로 연동하려면 파드 IP가 아닌. 서로를 발견(Discovery)할 수 있는 다른 방법이 필요하다.

도커 사용 방법을 되살펴보면 도커 컨테이너는 -p(publish) 옵션으로 손쉽게 컨테이너를 외부로 노출할 수 있었다.
즉, 컨테이너가 생성됨과 동시에 외부로 노출되는 방식이었다.
또한 오버레이 네트워크나 도커 사용자 정의 네트워크, docker run --link 옵션으로 컨테이너들이 서로를 이름으로 접근할 수도 있었다.

그렇지만 쿠버네티스에서는 파드에 접근하도록 정의하는 방법이 도커와 약간 다르다.
docker run -p 명령어와는 달리 쿠버네티스는 디플로이먼트를 생성할 때 파드를 외부로 노출하지 않으며, 디플로이먼트의 YAML 파일에는 단지 파드의 애플리케이션이 사용할 내부 포트만 정의한다.
이전의 Nginx 디플로이먼트를 생성했을 때 사용했던 YAML 파일 중에서 containerPort 항목이 바로 그것이다.
Nginx는 80 포트로 웹 서버를 제공하기 때문에 containerPort의 값을 80으로 설정했다.

...
spec:
  containers:
  - name: nginx
    image: nginx:1.10
    ports:
    - containerPort: 80 
...

그러나 YAML 파일에서 containerPort 항목을 정의했다고 해서 이 파드가 바로 외부로 노출되는 것은 아니다.

이 포트를 외부로 노출해 사용자들이 접근하거나, 다른 디플로이먼트의 파드들이 내부적으로 접근하려면 서비스(service)라고 부르는 별도의 쿠버네티스 오브젝트를 생성해야 한다.
서비스는 파드에 접근하기 위한 규칙을 정의하기 때문에 쿠버네티스에서 애플리케이션을 배포 하기 위해서는 반드시 알아야 할 오브젝트이다.
서비스에는 다양한 기능이 있지만, 핵심 기능만 나열해보면 다음과 같다.

  • 여러 개의 파드에 쉽게 접근할 수 있도록 고유한 도메인 이름을 부여한다.
  • 여러 개의 파드에 접근할 때, 요청을 분산하는 로드 밸런서 기능을 수행한다.
  • 클라우드 플랫폼의 로드 밸런서, 클러스터 노드의 포트 등을 통해 파드를 외부로 노출한다.

위에 언급한 기능들은 자주 사용하는 기능들이다 그렇지만, 서비스의 기능은 다양한 용도로 사용될 수 있다.
지금은 애플리케이션을 구성하는 데 필수적인 위 3가지 기능들을 위주로 살펴본다.


참고

쿠버네티스를 설치할 때 기본적으로 calico, flannel 등의 네트워크 플러그인을 사용하도록 설정되기 때문에 자동으로 오버레이 네트워크를 통해 각 파드끼리 통신할 수 있다. 단, 어떠한 네트워크 플러그인을 사용하느냐에 따라 네트워킹 기능 및 성능에 차이가 있을 수 있다. 이 책에서는 calico 를 기준으로 설명하지만, 다른 네트워크 플러그인을 사용해도 쿠버네티스의 핵심 기능을 사용하는 데에는 큰 문제가 없다.


2-1. 서비스(Service)의 종류

서비스의 개념과 사용 방법을 익히기 위해 파드와 서비스를 연결해 보자.
서비스를 생성하기에 앞서 아래의 YAML 파일을 이용해 먼저 디플로이먼트를 생성한다.
이번에는 컨테이너(파드)의 호스트 이름을 반환하는 간단한 웹 서버 이미지를 사용한다.

deployment-hostname.yaml

apiVersion: apps/v1 
kind: Deployment 
metadata: 
    name: hostname-deployment 
spec: 
    replicas: 3 
    selector: 
        matchLabels: 
            app: webserver
     template: 
         metadata: 
             name: my-webserver 
             labels: 
                 app: webserver 
         spec: 
             containers: 
             - name: my-webserver 
               image: alicek106/rr-test:echo-hostname 
               ports:
               - containerPort: 80
$ kubectl apply -f deployment-hostname.yaml
deployment.apps/hostname-deployment created

각 파드에서 실행 중인 웹 서버는 파드의 호스트 이름을 반환하는 단순한 동작을 수행한다.

kubectl get pods -o wide 명령어를 이용해 파드의 IP를 확인한 뒤, curl 등과 같은 도구로 HTTP 요청을 보내 파드의 이름을 확인할 수 있다.

$ kubectl get pods
NAME                                      READY   STATUS    RESTARTS   AGE
hostname-deployment-96df6b9c4-85xsg       1/1     Running   0          19s
hostname-deployment-96df6b9c4-8rmz6       1/1     Running   0          19s
hostname-deployment-96df6b9c4-rr4kb       1/1     Running   0          19s
$ kubectl get pods -o wide
NAME                                      READY   STATUS    RESTARTS   AGE   IP
              NODE          NOMINATED NODE   READINESS GATES
hostname-deployment-96df6b9c4-85xsg       1/1     Running   0          32s   192
.168.11.147   kuber-work2   <none>           <none>
hostname-deployment-96df6b9c4-8rmz6       1/1     Running   0          32s   192
.168.11.148   kuber-work2   <none>           <none>
hostname-deployment-96df6b9c4-rr4kb       1/1     Running   0          32s   192
.168.11.75    kuber-work1   <none>           <none>
$ # 클러스터의 노드 중 하나에 접속해, 노드에서 curl을 통해 파드로 접근해도 된다. 
$ kubectl run -i --tty --rm debug --image=alicek106/ubuntu:curl --restart=Never curl 192.168.11.147 | grep Hello

<p>Hello,  hostname-deployment-96df6b9c4-85xsg</p>      </blockquote>

파드에 접근할 수 있는 규칙을 정의하는 서비스 리소스를 새롭게 생성해 보자.
한 가지 알아둬야 할 점은 쿠버네티스의 서비스는 파드에 어떻게 접근할 것이냐에 따라 종류가 여러 개로 세분화 돼있다.

따라서 목적에 맞는 적절한 서비스의 종류를 선택해야 한다.
서비스의 종류에 따라 파드에 접근할 수 있는 방법이 달라지기 때문에 서비스의 종류는 반드시 알아둬야 한다.

서비스는 여러 가지 종류가 있으나, 주로 사용하는 서비스 타입은 크게 3가지가 있다.

  • ClusterIP 타입
    • 쿠버네티스 내부에서만 파드들에 접근할 때 사용한다.
    • 외부로 파드를 노출하지 않기 때문에 쿠버네티스 클러스터 내부에서만 사용되는 파드에 적합하다.
  • NodePort 타입
    • 파드에 접근할 수 있는 포트를 클러스터의 모든 노드에 동일하게 개방한다.
    • 따라서 외부에서 파드에 접근할 수 있는 서비스 타입이다.
    • 접근할 수 있는 포트는 랜덤으로 정해지지만, 특정 포트로 접근하도록 설정할 수도 있다.
  • LoadBalancer 타입
    • 클라우드 플랫폼에서 제공하는 로드 밸런서를 동적으로 프로비저닝해 파드에 연결한다.
    • NodePort 타입과 마찬가지로 외부에서 파드에 접근할 수 있는 서비스 타입이다.
    • 그렇지만 일반적으로 AWS, GCP 등과 같은 클라우드 플랫폼 환경에서만 사용할 수 있다.

예를 들어, 앞서 생성했던 파드를 내부에서만 접근하고 싶다면 ClusterIP 타입의 서비스를 사용할 수 있을 것이다.
그렇지만 외부에서도 파드에 접근하고 싶다면 NodePort 타입을,
실제 운영 환경 에서는 LoadBalancer 타입을 사용하면 된다.

이처럼 파드에 접근하는 방식 및 환경에 따라서 적절한 종류를 선택해야 한다.

2-2. ClusterIP 타입의 서비스 - 쿠버네티스 내부에서만 파드에 접근하기

지금 당장 모든 서비스의 종류와 특징을 세세하게 알 필요는 없다.
지금은 가장 간단하게 사용해 볼 수 있는 ClusterIP 타입의 서비스를 먼저 사용해보자.
아래의 내용으로 hostname-svc-clusterip.yaml 파일을 작성하자.

apiVersion: v1 
kind: Service 
metadata: 
    name: hostname-svc-clusterip 
spec: 
    ports: 
      - name: web-port 
        port: 8080 
        targetPort: 80 
    selector:
      app: webserver 
    type: ClusterIP

서비스를 정의하는 YAML 파일의 항목을 간단히 살펴보면 다음과 같다.

  • spec.selector
    • selector 항목은 이 서비스에서 어떠한 라벨을 가지는 파드에 접근할 수 있게 만들 것인지 결정한다.
    • 위 예시에서는 app: webserver 라는 라벨을 가지는 파드들의 집합에 접근할 수 있는 서비스를 생성한다.
    • deployment-hostname.yaml 파일로 생성된 디플로이먼트의 파드는 이 라벨이 설정돼 있으므로 이 서비스에 의해 접근 가능한 대상으로 추가될 것이다.
  • spec.ports.port
    • 생성된 서비스는 쿠버네티스 내부에서만 사용할 수 있는 고유한 IP(ClusterlP)를 할당받는다.
    • port 항목에는 서비스의 IP에 접근할 때 사용할 포트를 설정한다.
  • spec.ports.targetPort
    • selector 항목에서 정의한 라벨에 의해 접근 대상이 된 파드들이 내부적으로 사용하고 있는 포트를 입력한:
    • deployment-hostname.yaml 파일의 containerPort 항목에서 파드가 사용할 포트를 80으로 선언했기 때문에 위의 ports.targetPort 항목을 80으로 설정했다.
    • 즉 파드 템플릿에 정의된 containerPort 와 같은 값으로 설정해야 한다.
  • spec.type
    • 0| 서비스가 어떤 타입인지 나타낸다.
    • 서비스의 종류에는 ClusterlP, NodePort, LoadBalancer 등을 설정 할 수 있다.
    • 지금은 서비스의 개념을 이해하기 위해 가장 간단한 서비스 종류인 ClusterIP를 사용하고 있다.

참고

레플리카셋이나 서비스의 selector처럼 두 리소스 간의 라벨이 동일할 때에만 쿠버네티스의 기능을 온전히 사용할 수 있는 경우가 앞으로 자주 등장할 것이다.
쿠버네티스에서의 라벨은 단순히 리소스의 부가적인 정보를 표시하는 것 이상의 기능을 가질 수도 있다는 점을 알아두자.


위 YAML 파일을 이용해 서비스를 생성해 보자.
이전에 리소스를 생성했던 방법과 동일하게 kubectl apply -f 명령어를 사용한다.

$ kubectl apply -f hostname-svc-clusterip.yaml
service/hostname-svc-clusterip created

생성된 서비스의 목록을 확인해 보자.

$ kubectl get services
$ kubectl get svc # services 대신 svc라는 이름으로도 사용가능
NAME                     TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)
   AGE
hostname-svc-clusterip   ClusterIP   10.110.110.199   <none>        8080/TCP
   43s
kubernetes               ClusterIP   10.96.0.1        <none>        443/TCP
   5d2h

참고

생성한 적이 없는데도 kubernetes 라는 이름의 서비스가 미리 생성돼 있다.
이 서비스는 파드 내부에서 쿠버네티스의 API에 접근하기 위한 서비스이다.
이 서비스에 대한 자세한 내용은 추후 다룬다.


ClusterIP 타입의 hostname-svc-clusterip라는 이름의 서비스가 생성돼있다.
이 서비스를 사용해 파드에 접근하는 방법은 매우 간단하다.
위 출력 내용 중 CLUSTER-IP 항목의 IPPORT(S) 항목의 포트를 통해 요청을 보내면 된다.

IP 는 쿠버네티스 클러스터에서만 사용할 수 있는 내부 IP로, 이 IP를 통해 서비스에 연결된 파드에 접근할 수 있다.
쿠버네티스 클러스터의 노드 중 하나에 접속해 위 IP로 요청을 보내도 되지만,
kubectl run 명령어로 임시 파드를 만들어 요청을 전송해자

$ kubectl run -i --tty --rm debug --image=alicek106/ubuntu:curl --restart=Never -- bash

If you don't see a command prompt, try pressing enter.
root@debug:/# curl 10.110.110.199:8080 --silent | grep Hello
        <p>Hello,  hostname-deployment-96df6b9c4-85xsg</p>      </blockquote>
root@debug:/# curl 10.110.110.199:8080 --silent | grep Hello
        <p>Hello,  hostname-deployment-96df6b9c4-8rmz6</p>      </blockquote>
root@debug:/# curl 10.110.110.199:8080 --silent | grep Hello
        <p>Hello,  hostname-deployment-96df6b9c4-rr4kb</p>      </blockquote>

서비스의 IP와 포트를 통해 파드에 접근할 수 있음을 알 수 있다.
게다가 서비스와 연결된 여러 개의 파드에 자동으로 요청이 분산되고 있다.

서비스를 생성할 때 별도의 설정을 하지 않아도 서비스는 연결된 파드에 대해 로드 밸런싱을 수행한다.

서비스에는 IP 뿐만 아니라 서비스 이름 그 자체로도 접근할 수 있다.
쿠버네티스는 애플리케이션이 서비스나 파드를 쉽게 찾을 수 있도록 내부 DNS 를 구동하고 있으며, 파드들은 자동으로 이 DNS를 사용하도록 설정되어 있기 때문이다.

root@debug:/# curl hostname-svc-clusterip:8080 --silent | grep Hello
        <p>Hello,  hostname-deployment-96df6b9c4-rr4kb</p>      </blockquote>

실제로 여러 파드가 클러스터 내부에서 서로를 찾아 연결해야 할 때는 서비스의 이름과 같은 도메인 이름을 사용하는 것이 일반적이다.
즉, 파드가 서로 상호작용해야 할 때는 파드의 IP를 알 필요가 없으며, 대신 파드와 연결된 서비스 이름을 사용함으로써 간단히 파드에 접근할 수 있다.
위처럼 ClusterIP 타입의 서비스를 생성해 파드에 접근하는 과정을 다시 정리해 보자.

  1. 특정 라벨을 가지는 파드를 서비스와 연결하기 위해 서비스의 YAML 파일에 selector 항목을 정의한다.
  2. 파드에 접근할 때 사용하는 포트(파드에 설정된 containerPort)를 YAML 파일의 targetPort 항목에 정의한다.
  3. 서비스를 생성할 때, YAML 파일의 port 항목에 8080을 명시해 서비스의 Cluster IP8080 포트로 접근할 수 있게 설정한다.
  4. kubectl apply -f 명령어로 ClusterIP 타입의 서비스가 생성되면 서비스는 쿠버네티스 클러스터 내부에서만 사용할 수 있는 고유한 내부 IP를 할당받는다.
  5. 쿠버네티스 클러스터에서 서비스의 내부 IP 또는 서비스 이름으로 파드에 접근할 수 있다.

단, 위에서 생성한 서비스는 ClusterIP 타입이기 때문에 외부에서는 접근할 수 없다는 점에 유의해야 한다.
클러스터 내부에서만 사용하는 파드라면 상관없지만, 외부에 노출해야 한다면 뒤에서 설명할 NodePortLoadBalancer 타입의 서비스를 생성해야 한다.


참고

서비스의 라벨 셀렉터(selector)와 파드의 라벨이 매칭돼 연결되면 쿠버네티스는 자동으로 엔드포인트(endpoint)라고 부르는 오브젝트를 별도로 생성한다.
예를 들어, 위에서 생성한 서비스와 관련된 엔드포 인트는 서비스와 동일한 이름으로 존재하고 있다.

$ kubectl get endpoints
$ kubectl get ep # 또는 ep라는 이름으로도 사용 가능
NAME                     ENDPOINTS                                                    AGE
hostname-svc-clusterip   192.168.11.147:80,192.168.11.148:80,192.168.11.75:80         48m
kubernetes               192.168.9.211:6443                                           5d3h

엔드포인트라는 이름이 의미하는 것처럼 엔드포인트 오브젝트는 서비스가 가리키고 있는 도착점(endpoint)을 나타낸다.

서비스를 이용해 파드를 연결한다면 엔드포인트는 자동으로 생성되므로 엔드포인트를 자세하게 알 필요는 없다.
그렇지만 엔드포인트 자체도 독립된 쿠버네티스의 리소스이기 때문에 이론상으로는 서비스와 엔드포인트를 따로 만드는 것도 가능하다.


서비스를 삭제하려면 kubectl delete 명 령어를 사용힌다.

$ kubectl delete svc hostname-svc-clusterip 
$ kubectl delete -f hostname-svc-clusterip.yaml

2-3. NodePort 타입의 서비스 - 서비스를 이용해 파드를 외부에 노출하기

ClusterIP 타입의 서비스는 내부에서만 접근 가능하지만, NodePort 타입의 서비스는 클러스터 외부에서도 접근할 수 있다.

단 NodePort라는 이름에서 알 수 있듯이 NodePort 타입의 서비스는 모든 노드(Node)의 특정 포트(Port)를 개방해 서비스에 접근하는 방식이다.
스웜 모드에서 컨테이너를 외부로 노출하는 방식과 비슷하다고 생각하면 쉽다.
NodePort 타입의 서비스를 생성하기 위해 아래의 내용으로 YAML 파일을 작성한다.

hostname-svc-nodeport.yaml

apiVersion: v1 
kind: Service 
metadata:
  name: hostname-svc-nodeport 
spec: 
  ports:
    - name: web-port 
      port: 8080 
      targetPort: 80 
  selector: 
    app: webserver 
  type: NodePort

ClusterIP 타입의 서비스를 생성했을 때 사용한 YAML 파일과 비교했을 때, type 항목을 NodePort 로 설정한 점을 제외하고는 모두 동일하다.
NodePortClusterIP와 동작 방법이 다른 것일 뿐 동일한 서비스 리소스이기 때문에 라벨 셀렉터, 포트 설정 등과 같은 기본 항목의 사용 방법은 모두 같다.
작성한 YAML 파일을 이용해 NodePort 타입의 서비스를 생성해 보자.

$ kubectl apply -f hostname-svc-nodeport.yaml
service/hostname-svc-nodeport created
$ kubectl get services
NAME                     TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
hostname-svc-nodeport    NodePort    10.100.114.25    <none>        8080:31649/TCP   2m29s
kubernetes               ClusterIP   10.96.0.1        <none>        443/TCP        5d3h

서비스의 목록을 확인해보면 NodePort 타입의 서비스가 생성됐음을 알 수 있다.

PORT(S) 항목에 출력된 31649라는 숫자는 모든 노드에서 동일하게 접근할 수 있는 포트를 의미한다.
즉, 클러스터의 모든 노드에 내부 IP 또는 외부 IP를 통해 31649 포트로 접근하면 동일한 서비스에 연결할 수 있다.

$ kubectl get nodes -o wide 
NAME           STATUS   ROLES           AGE    VERSION   INTERNAL-IP     EXTERNAL-IP   OS-IMAGE           KERNEL-VERSION     CONTAINER-RUNTIME
kuber-node   Ready    control-plane   5d3h   v1.32.2   10.43.0.20   <none>        Ubuntu 24.04 LTS   6.8.0-31-generic   containerd://1.7.25
...    Ready    <none>          5d3h   v1.32.2   10.43.0.21    <none>        Ubuntu 24.04 LTS   6.8.0-31-generic   containerd://1.7.25
...    Ready    <none>          5d3h   v1.32.2   10.43.0.22   <none>        Ubuntu 24.04 LTS   6.8.0-31-generic   containerd://1.7.25
$ curl 192.168.9.212:31649  --silent | grep Hello
        <p>Hello,  hostname-deployment-96df6b9c4-8rmz6</p>      </blockquote>
$ curl 192.168.9.212:31649  --silent | grep Hello
        <p>Hello,  hostname-deployment-96df6b9c4-rr4kb</p>      </blockquote>
$ curl 192.168.9.212:31649  --silent | grep Hello
        <p>Hello,  hostname-deployment-96df6b9c4-85xsg</p>      </blockquote>

단 GKE에서 쿠버네티스를 사용하고 있는 경우, 각 노드의 랜덤한 포트에 접근하기 위해 별도로 방화벽 설정을 추가해야 한다.
또한, AWS에서도 마찬가지로 Security Group에 별도의 Inbound 규칙을 추가하지 않으면 NodePort 로 통신이 실패할 수 있다

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

참고

각 노드에서 개방되는 포트는 기본적으로 30000〜32768 포트 중에 랜덤으로 선택되지만, YAML 파일에 nodePort 항목을 정의하면 원하는 포트를 선택할 수도 있다.

spec:
  ports:
  - name: web-port 
    port: 8080 
    targetPort: 80 
    nodePort: 31000

그런데 한 가지 이상한 점이 있다.
NodePort 타입의 서비스인데도 kubectl get service 명령어의 출력에서 CLUSTER-IP 항목에 내부 IP가 할당됐기 때문이다.
ClusterIP 타입의 서비스에 접근했을 때와 마찬가지로 NodePort 타입 서비스의 내부 IP로 접근해 보자.

$ kubectl run -i --tty --rm debug --image=alicek106/ubuntu:curl --restart=Never -- bash
If you don\'t see a command prompt, try pressing enter.
root@debug:/# curl 10.100.114.25:8080 --silent | grep Hello
        <p>Hello,  hostname-deployment-96df6b9c4-8rmz6</p>      </blockquote>
root@debug:/# curl 10.100.114.25:8080 --silent | grep Hello
        <p>Hello,  hostname-deployment-96df6b9c4-85xsg</p>      </blockquote>

이는 사실 NodePort 타입의 서비스가 ClusterIP의 기능을 포함하고 있기 때문이다.
NodePort 타입의 서비스를 생성하면 자동으로 ClusterIP의 기능을 사용할 수 있기 때문에 쿠버네티스 클러스터에서 서비스의 내부 IP와 DNS 이름을 사용해 접근할 수 있다.
즉, NodePort 타입의 서비스는 내부 네트워크와 외부 네트워크 양쪽에서 접근할 수 있다.

이를 다시 그림으로 정리해보면 다음과 같다.

  1. 외부에서 파드에 접근하기 위해 각 노드에 개방된 포트로 요청을 전송한다. 예를 들어, 위 그림에서 31514 포트로 들어온 요청은 서비스와 연결된 파드 중 하나로 라우팅된다.
  2. 클러스터 내부에서는 ClusterIP 타입의 서비스와 동일하게 접근할 수 있다.

참고

기본적으로 NodePort가 사용할 수 있는 포트 범위는 30000〜32768이지만, API 서버 컴포넌트의 실행 옵션을 변경하면 원하는 포트 범위를 설정할 수 있다.
포트 범위를 직접 지정하려면 API 서버의 옵션을 다음과 같이 추가하거나 수정하면된다.

--service-node-port-range=30000-35000

너무 낮은 포트 번호는 시스템에 으|해 예약된 포트일 수 있기 때문에 가능하면 기본적으로 설정된 30000 번 이상의 포트를 사용하는 것이 좋다.


실제 운영 환경에서 NodePort로 서비스를 외부에 제공하는 경우는 많지 않다.
NodePort에서 포트 번호를 80 또는 443으로 설정하기에는 적절하지 않으며, SSL 인증서 적용, 라우팅 등과 같은 복잡한 설정을 서비스에 적용하기가 어렵기 때문이다.

따라서 NodePort 서비스 그 자체를 통해 서비스를 외부로 제공하기보다는 인그레스(Ingress)라고 부르는 쿠버네티스의 오브젝트에서 간접적으로 사용되는 경우가 많다.

인그레스 오브젝트에 대해서는 지금은 '외부 요청을 실제로 받아들이는 관문' 정도로 알고 넘어가자.

아래에서 설명할 LoadBalancerNodePort를 합치면 인그레스 오브젝트를 사용할 수 있을 것이다.


참고

특정 클라이언트가 같은 파드로부터만 처리되게 하려면 서비스의 YAML 파일에서 sessionAffinity 항목을 ClientIP 로 설정한다.

...
spec:
  sessionAffinity: ClientIP
...

2-4. 클라우드 플랫폼의 로드 밸런서와 연동하기 - LoadBalancer 타입의 서비스

LoadBalancer 타입의 서비스는 서비스 생성과 동시에 로드 밸런서를 새롭게 생성해 파드와 연결한다.

NodePort를 사용할 때는 각 노드의 IP를 알아야만 파드에 접근할 수 있었지만, 이번에 사용해 볼 LoadBalancer 타입의 서비스는 클라우드 플랫폼으로부터 도메인 이름과 IP를 할당받기 때 문에 NodePort 보다 더욱 쉽게 파드에 접근할 수 있다.

단, LoadBalancer 타입의 서비스는 로드 밸런서를 동적으로 생성하는 기능을 제공하는 환경에서만 사용할 수 있다는 점을 알아둬야 한다.

일반적으로 AWS, GCP 등과 같은 클라우드 플랫폼 환경에서만 LoadBalancer 타입을 사용할 수 있으며, 가상 머신이나 온프레미스 환경에서는 사용하기 어려울 수 있다.

이후 내용은 AWS 혹은 GCP에서 쿠버네티스를 사용하는 환경을 가정하고 설명하므로, 필자가 같은 환경을 다루게 될 때 다시 보기로 한다.


참고

쿠버네티스에서 주석(annotations)은 라벨처럼 해당 리소스의 추가적인 정보를 나타내기 위한 키-값 쌍으 로 이뤄져 있다.
그렇지만 리소스의 종류에 따라 특정 용도로 사용할 수 있게 쿠버네티스에 미리 정의된 몇 가지 주석이 있다.
이 주석들은 리소스에 특별한 설정값을 부여하기 위해 사용된다.
예를 들면, service.beta.kubernetes.io/aws-load-balancer-type 이라는 키의 값을 nlb로 설정함으로써 클래식 로드 밸런서가 아닌 네트워크 로드 밸런서를 생성하도록 설정할 수 있다.

미리 정의된 특별한 주석은 라벨과 함께 익숙해지는 것이 좋다.


로드 밸런서의 사용을 마쳤다면 해당 서비스를 삭제한다면 생성된 로드 밸런서 또한 함께 삭제된다.

온프레미스 환경에서 LoadBalancer 타입의 서비스 사용하기

LoadBalancer 타입의 서비스는 일반적으로 AWS와 같은 클라우드 플랫폼에서 사용되지만, 필요하다면 직접 보유하고 있는 온프레미스 서버에서도 LoadBalancer 타입을 사용할 수 있다.

단, 쿠버네티스가 이 기능을 직접 제공하는 것은 아니며. MetalLB나 오픈스택과 같은 특수한 환경을 직접 구축해야만 한다.
그중에서도 MetalLB라는 이름의 오픈소스 프로젝트를 사용하면 쉽게 LoadBalancer 타입의 서비스를 사용할 수 있으나, MetalLB는 쿠버네티스의 공식 프로젝트가 아니며 유지보수가 지속적이지 않을 수 있다는 점에 유의해야 한다.

관심이 있다면 MetalLB의 설치를 설명하는 공식 문서를 참고하자.

2-5. 트래픽의 분배를 결정하는 서비스 속성: externalTrafficPolicy

LoadBalancer 타입의 서비스를 사용하면 외부로부터 들어온 요청은 각 노드 중 하나로 보내지며, 그 노드에서 다시 파드 중 하나로 전달된다.
마찬가지로 NodePort 타입을 사용했을 때도 각 노드로 들어오는 요청은 다시 파드 중 하나로 전달된다.
그렇지만 이러한 요청 전달 원리는 경우에 따라 효율적이지 않을 때도 있다.
아래 그림과 같은 상황을 가정해 보자.

LoadBalancer 또는 NodePort에 의해 모든 노드에서 31000번 포트가 개방되어 파드에 접근할 수 있으며, 워커 노드 A와 B에 파드가 각각 생성돼 있다고 가정해 보자.

이때 워커 노드 A로 들어오는 요청은 (1) A에 위치한 a 파드 또는 (2) B에 위치한 b 파드 중 하나로 전달될 것이다.

주목해야 할 부분은 A 노드로 들어오는 요청은 굳이 B 노드로 전달되지 않고 A 노드 내부 (a 파드)에서 처리될 수 있다는 점이다.
그런데도 A 노드로 들어온 요청이 b 파드로 전달되어 처리되면 불필요한 네트워크 홉(hop)이 한 단계 더 발생하게 된다.
게다가 노드간의 리다이렉트가 발생하게 되어 트래픽의 출발지 주소가 바뀌는 SNAT가 발생하게 되고, 이로 인해 클라이언트의 IP 주소 또한 보존되지 않는다는 단점이 있다.
이러한 요청 전달 메커니즘은 서비스의 속성 중 externalTrafficPolicy 항목에 정의돼 있다.

kubectl get -o yaml 명령어로 서비스의 모든 속성을 출력해 보면 externalTrafficPolicy가 Cluster로 설정된 것을 알 수 있다.

$ kubectl get svc hostname-svc-nodeport -o yaml
apiVersion: v1
kind: Service
metadata:
  annotations:

...
  externalTrafficPolicy: Cluster
...

참고

kubectl get 에서 -o 옵션을 이용해 리소스의 정보를 yaml, json 등의 형식으로 출력할 수 있다.
-o 옵션으로 출력하면 쿠버네티스에 의해 자동으로 설정된 상세 항목들까지 모두 확인할 수 있다.


이처럼 externalTrafficPolicy에서 기본적으로 설정된 값인 Cluster는 클러스터의 모든 노드에 랜덤한 포트를 개방하는, 앞서 우리가 사용해봤던 NodePortLoadBalancer 타입의 서비스가 기본적으로 동작하는 방식이다.
그러나 externalTrafficPolicy를 Local로 설정하면 파드가 생성된 노드에서만 파드로 접근할 수 있으며, 로컬 노드에 위치한 파드 중 하나로 요청이 전달된다.
즉, 추가적인 네트워크 홉이 발생하지 않으며, 전달되는 요청의 클라이언트 IP 또한 보존된다.
이번에는 externalTrafficPolicy가 Local 인 로드 밸런서 서비스를 생성해 보자.
아래의 내용으로 hostname-svc-lb-local.yaml 파일을 작성해 서비스를 생성하자.

apiVersion: v1 
kind: Service 
metadata: 
  name: hostname-svc-lb-local
spec: 
  externalTrafficPolicy: Local 
  ports:
    - name: web-port 
      port: 80 
      targetPort: 80 
  selector:
    app: webserver 
  type: LoadBalancer
$ kubectl apply -f hostname-svc-lb-local.yaml
service/hostname-svc-lb-local created

위에서 생성했던 hostname-deployment를 계속 사용하고 있다면 3개의 파드를 1개로 줄여 한 개의 노드에서만 파드가 존재하도록 변경해 보자.
kubectl scale 명령어를 사용하면 디플로이먼트의 파드 개수를 줄일 수 있다.

$ kubectl scale --replicas=1 deployment hostname-deployment
deployment.apps/hostname-deployment scaled
$ kubectl get deploy
NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
hostname-deployment      1/1     1            1           134m
$ kubectl get pods -o wide
NAME                                      READY   STATUS    RESTARTS   AGE    IP               NODE          NOMINATED NODE   READINESS GATES
hostname-deployment...       1/1     Running   0          134m   192.168.11.75    ip-10-43-0-31..   <none>           <none>

위 예시에서는 ip-10-43-0-31..이라는 워커 노드에만 1개의 파드가 존재하고 있다.
단, 서비스를 생성할 때 externalTrafficPolicyLocal 로 설정했기 때문에ip-10-43-0-31.. 노드에서만 이 파드에 접근할 수 있을 것이다.
개방된 포트 번호를 kubectl get services 명령어로 확인한 다음, 각 노드로 요청을 보내 보면 이를 알 수 있다.

$ kubectl get svc
NAME                     TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
hostname-svc-lb-local    LoadBalancer   10.107.74.103    <pending>     80:30415/TCP     4m34s

# 워커노드로만 요청한다.
$ curl 10.43.0.22:30415 --silent | grep Hello
   <p>Hello,  hostname-deployment-96df6b9c4-rr4kb</p>      </blockquote>
$ curl 10.43.0.23:30415 --silent | grep Hello
^C # 응답 없음

만약 AWS에서 쿠버네티스를 사용하고 있다면 로드 밸런서 페이지에서 단 하나의 노드만이 InService 상태로 되어있으며 이 노드로만 트래픽이 전달되는 것을 확인할 수 있을 것이다.
즉, 로드 밸런서는 파드가 존재하지 않는 노드로는 요청을 전달하고 있지 않는 것이다.

extemalTrafficPolicy의 값이 Local로 설정된 서비스의 트래픽 흐름을 다시 그림으로 살펴자.

  1. 서비스는 기본적으로 extemalTrafficPolicy 속성이 Cluster로 설정된다. 이 경우 모든 워커 노드에서 동일한 랜덤 포트가 개방된다. 클라우드 플랫폼의 로드 밸런서는 노드 중 하나로 요청을 전달하고, 노드는 다시 파드 중 하나로 요청을 전달한다. 단 노드 간에 요청이 리다이렉트 되어 NAT가 발생하므로 클라이언트의 IP를 보존할 수 없다.
  2. 서비스의 extemalTrafficPolicy 속성을 Local로 설정해 생성하면 파드가 위치한 노드만 랜덤한 포트를 개방한다. 로드 밸런서는 파드가 위치한 노드로만 요청을 전달하며, 해당 노드 내의 파드에서만 요청이 분산된다. 따라서 네트워크 홉이 한 단계 적으며, 클라이언트의 IP 또한 파드의 소스크드 내에서 정상적으로 확인할 수 있다.

그렇지만 extemalTrafficPolicyLocal로 설정하는 것이 무조건 좋은 것은 아니다.
각 노드에 파드가 고르지 않게 스케줄링됐을 때 요청이 고르게 분산되지 않을 수도 있기 때문이다.
만약 아래와 같은 상황이라면 어떨까..???

위 그림에서는 로드 밸런서가 두 개의 노드에 대해 트래픽을 절반씩 분배하지만 각 파드가 실제로 받는 부하의 양은 동일하지 않다.
즉. 특정 노드의 파드에 부하가 집중되거나 적어질 수도 있으며, 이는 곧 자원 활용률(utilization) 측면에서 바람직하지 않을 수도 있다는 것을 의미한다.


참고
쿠버네티스 스케줄링 기능 중 PodAntiAffinity 등을 사용하면 파드를 최대한 클러스터 노드에 고르게 배포할 수 있으며, externalTrafficPolicy: Local의 단점을 어느 정도 해결할 수 있다.


Cluster와 Local은 둘 다 장단점이 있기 때문에 뚜렷한 정답은 없다.
불필요한 네트워크 홉으로 인한 레이턴시나 클라이언트의 IP 보존이 중요하지 않다면 Cluster를 사용해도 된다.
그 반대라면 Local을 사용하는 것이 좋은 선택일 수도 있다.

2-6. 요청을 외부로 리다이렉트하는 서비스 : ExternalName

앞으로 서비스를 사용할 때는 앞서 언급한 ClusterIP, NodePort, LoadBalancer 세 가지만 알아도 충분하지만, 쿠버네티스를 외부 시스템과 연동해야 할 때는 ExternalName 타입의 서비 스를 사용할 수도 있다.
ExternalName 타입을 사용해 서비스를 생성하면 서비스가 외부 도메인을 가리키도록 설정할 수 있다.
예를 들어 아래의 설정은 쿠버네티스 내부의 파드들이 externaIname-svc 라는 이름으 로 요청을 보낼 경우, 쿠버네티스의 DNS는 my.database.com으로 접근할 수 있도록 CNAME 레코드를 반환한다.

externalname-svc로 요청을 보내면 my.database.com에 접근하게 된다.
ExternalName 타입의 서비스는 쿠버네티스와 별개로 존재하는 레거시 시스템에 연동해야 하 는 상황에서 유용하게 사용할 수 있다.

apiVersion: v1 
kind: Service 
metadata: 
  name: externalname-svc 
spec: 
  type: ExternalName 
  externalName: my.database.com

참고

DNS 레코드 종류 중 CNAME 레코드는 Canonical Name의 줄임말로, 도메인을 가리키는 다른 이름을 뜻한다.
이와 비슷한 레코드로는 A 레코드가 있는데, A 레코드는 도메인 이름이 직접 IP로 변환(resolve)되는 경우를 의미한다.

사용하는 도메인 이름 변환되는 주소
externalname-svc(CNAME 레코드) my.datanase.com
hostname-svc-nodeport 10.110.9.251

3. 요약

  • 쿠버네티스의 서비스에서 자주 쓰이는 3가지에 대해서 자세히 알아보았다.
  • 용도에 맞게 어떤 서비스를 써야하는지 알아보았다.
  • 단 AWS 에서 사용하는 로드밸런스 부분은 생략되었다.
728x90
Comments