본문 바로가기
Infra/Kubernetes

[Kubernetes] Service - ClusterIP, NodePort, LoadBalancer

by 진꿈청 2024. 8. 27.

Kubernetes

 

 

Service

 

 

서비스는 기본적으로 자신의 `클러스터 IP`를 가지고 있다.

그리고 이 서비스를 파드에 연결을 시켜 놓으면 서비스의 IP를 통해서 파드에 접근이 가능하다.

 

그런데, 전 포스팅에서 살펴본 것처럼 파드도 똑같이 클러스터 내에서 접근할 수 있는 IP가 있었다.

 

 

그렇다면 굳이 왜 서비스를 이용하는 것일까?

 

 

사용 이유

 

`Pod`라는 존재는 `Kubernetes`에서 시스템 장애건, 성능 장애건 언제든지 죽을 수가 있다.

그리고 그때 다시 재생성 되도록 설계가 되어있는 오브젝트이다.

 

근데 이때, `Pod`는 재생성시 IP가 변경된다. 따라서, 이 파드의 IP는 신뢰성이 떨어진다.

그러나, `Pod`와는 별개로 서비스는 사용자가 직접 지우지 않는 한 삭제되거나 재생성되지 않는다.

 

그래서 해당 서비스의 IP로 접근하면 항상 해당 서비스와 연결되어 있는 `Pod`에 접근이 가능하다.

 

위와 같은 이유로 서비스를 사용하는 것이고 서비스에는 몇 가지 종류가 존재한다.

서비스의 종류에 따라 파드의 접근을 도와주는 방식에 차이가 있는데 종류는 다음과 같다.

 

 

서비스의 종류

  • 클러스터 IP
  • NodePort
  • Load Balancer

 

순서대로 알아보자.

 

 

클러스터 IP

 

클러스터 IP는 `Kubernetes` 클러스터 내에서만 접근이 가능한 IP이다.

따라서, `Pod`에 있는 IP와 특징이 똑같다고 보면된다.(물론, 앞서 설명한 것처럼 Pod처럼 쉽게 재생성되진 않는다.)

 

 

해당 클러스터 IP는 클러스터 내의 다른 모든 오브젝트들이 접근을 할 수 있지만, 외부에서는 접근이 불가능하다.

 

그리고 파드를 하나만 연결하는 것이 아닌 여러 개의 파드를 연결할 수가 있는데, 파드를 여러 개 연결시키면

서비스가 알아서 트래픽을 분산하여 파드에 전달해준다.

 

실습에 앞서 서비스를 생성하는 YAML 파일을 살펴보면 다음과 같다.

 

apiVersion: v1
kind: Service
metadata:
  name: svc-1
spec:
  selector:
    app: pod
  ports:
  - port: 9000
    targetPort: 8080
  type: ClusterIP

 

위의 YAML 파일에서 마지막 라인을 보면 `type: ClusterIP`라고 되어있다.

해당 타입값은 옵션값이기에 생략을 하는 것이 가능하다.

만약, 생략을 하면 기본 값이 클러스터 IP이므로, 클러스터 IP를 사용할 때는 해당 타입을 넣지 않아도 된다.

 

그리고 위의 `ports`를 살펴보면 `9000`번 포트로 서비스에 요청을 하면 타겟이 되는 `Pod`의 `8080`포트로 연결이 된다.

 

 

NodePort

 

`NodePort` 타입으로 만들어도 서비스에는 기본적으로 클러스터 IP가 할당이 된다.

 

그렇기에 기본적으로 클러스터 IP 타입과 같은 기능이 포함되어 있다.

 

`NodePort` 타입만의 큰 특징은 `Kubernetes` 클러스터에 연결되어 있는 모든 노드한테 똑같은 포트가 할당이 된다.

따라서, 외부로부터 어느 노드던간에 그 IP의 포트로 접속을 하면 해당 서비스에 연결이 된다.

 

그러면 또 서비스는 기본 역할로 자신한테 연결되어 있는 `Pod`에 트래픽을 전달해준다.

 

이때, 주의할 점은 해당 `Pod`가 있는 노드에만 포트가 할당되는 것이 아닌 모든 노드에 포트가 만들어진다.

 

apiVersion: v1
kind: Service
metadata:
  name: svc-2
spec:
  selector:
    app: pod
  ports:
  - port: 9000
    targetPort: 8080
    nodePort: 30001
  type: NodePort
  externalTrafficPolicy: Local

 

`type`은 아까와 다르게 `NodePort` 타입이다. 이때 YAML 파일에 적혀있듯이 `NodePort`의 포트를 지정할 수 있다.

 

포트 범위: 30001 ~ 32767(30000는 대시보드 포트이다)

 

포트값을 지정하는 것또한 옵셔널이기 때문에 생성시 작성하지 않으면 자동으로 포트 범위 내에서 할당이 된다.

 

 

그리고 `NodePort` 서비스에는 한가지 더 추가적인 속성이 존재한다.

 

만약, 위의 그림처럼 각 노드에 `Pod`가 하나씩 올라가져 있다고 했을 때,

우리가 Node1IP로 접근을 하더라도 해당 서비스는 Node2에 있는 `Pod`한테 트래픽을 전달할 수 있다.

 

그 이유는 앞서 설명했던 것처럼 `NodePort`를 사용했을 때 특정 Node에 접근을 해도 결국 그것은 `NodePort`서비스에 전달된다.

그렇기에 서비스 입장에서는 어떤 Node한테 온 트래픽인지 상관없이 그냥 자신한테 달려있는 `Pod`들한테 트래픽을 전달하는

자신의 일을 하는 것이다.

 

 

근데 만약, YAML 파일 가장 아래에 있는 `External Traffic Policy`라는 값을 `Local`로 주게되면

특정 노드 포트로의 IP로 접근을 하는 트래픽은 서비스가 해당 노드에 위에 올려져 있는 `Pod`한테만 트래픽을 전달해준다.

 

 

 

LoadBalancer

`LoadBalancer` 타입의 서비스도 일단 기본적으로 앞에서 배운 `NodePort`의 성격을 또 그대로 가지고 있다.

그리고 추가적으로, 로드 밸런서라는게 생겨서 각각의 로드트래픽을 분산시켜주는 역할을 한다.

 

근데 한가지 문제는 해당 로드 밸런서에 접근을 하기 위한 외부 접속 IP 주소는 개별적으로 `Kubernetes`를 설치했을 때

기본적으로 생성되지 않는다. 별도로 외부 접속 IP를 할당을 해주는 플러그인이 설치가 되어 있어야 한다.

 

만약, GCP이나 아마존에서 제공하는 `Kubernetes` 플랫폼을 사용하면 자체적으로 플러그인이 설치가 되어 있어

`LoadBalancer` 타입으로 서비스를 만들면 알아서 외부에서 접속할 수 있게 IP를 만들어준다.

 

그럼 해당 IP를 통해 외부에서 접근이 가능하다.

 

LoadBalancer YAML 파일

apiVersion: v1
kind: Service
metadata:
  name: svc-3
spec:
  selector:
    app: pod
  ports:
  - port: 9000
    targetPort: 8080
  type: LoadBalancer

 

내용은 별거 없이 `type: LoadBalancer`를 지정해주면 된다.

 

 

여기까지 3가지 서비스 타입에 관해 알아보았는데 서비스는 이외에도 여러 용도로 더 쓰인다.

(해당 내용은 추후에 알아보자)

 

 

 

그러면 각각의 서비스 타입들을 어떤 상황에 적용해야 할까?

 

 

첫째로 클러스터 IP

 

클러스터 IP는 외부에서 접근할 수가 없고 클러스터 내에서만 사용되는 IP이다.

그렇기 때문에 해당 IP에 접근할 수 있는 대상은 클러스터 내부에 접근을 할 수 있는 운영자와 같은 인가된 사람일 수 밖에 없다.

주된 작업은 `Kubernetes` 대시보드를 관리하거나 각 `Pod`의 서비스 상태를 디버깅하는 작업을 위해 사용된다.

 

 

둘째로 NodePort

 

NodePort는 특징이 물리적인 호스트의 IP를 통해 `Pod`에 접근이 가능하다는 것인데,

대부분 호스트 IP는 보안적으로 내부망에서만 접근을 할 수 있게 네트워크를 구성한다.

 

따라서, 이 NodePort는 클러스터 밖에는 있지만 그래도 내부망 안에서 접근을 해야 될 때 사용된다.

 

그리고 일시적인 외부 연동용으로도 사용이 되는데 우리가 내부 환경에서 시스템 개발을 하다가

외부에 간단한 데모를 보여줘야 할 때 네트워크 중계기에 포트포워딩을 해서 NodePort를 잠깐 뚫어놓고 사용이 가능하다.

 

 

마지막으로 LoadBalancer

 

실제적으로 외부에 서비스를 노출시키려면 로드밸런서를 이용을 해야 된다.

그래야 내부 IP가 노출되지 않고 외부 IP를 통해 안정적으로 서비스를 노출시킬 수 있기 때문이다.

 

그렇기에 로드밸런서는 외부의 시스템을 노출하는 용으로 사용된다.

 

 

 

실습

몇 가지의 간단한 실습을 진행해보자.

 

우선, `Pod`가 죽어서 재생성 되더라도 서비스의 IP를 통해 접근하는 예제이다.

 

apiVersion: v1
kind: Pod
metadata:
  name: pod-1
  labels:
     app: pod
spec:
  nodeSelector:
    kubernetes.io/hostname: k8s-node1
  containers:
  - name: container
    image: kubetm/app
    ports:
    - containerPort: 8080

 

우선, 위의 YAML로 `Pod`를 하나 생성한다. 이때, 라벨은 `app: pod`로 지정을 해두었다.

 

 

그 후, 아래 YAML로 서비스를 하나 생성한다.

 

apiVersion: v1
kind: Service
metadata:
  name: svc-1
spec:
  selector:
    app: pod
  ports:
  - port: 9000
    targetPort: 8080

 

YAML 파일을 보면 알 수 있듯이 셀렉터로 방금 생성한 `Pod`의 라벨을 지정해주었다.

 

그렇게 서비스를 생성하면 다음과 같이 `Pod`와 연결된 상태로 잘 생성이 된다.

 

 

이때, 타입은 Cluster IP로 잘 생성이 되는데 `type` 값을 넣지 않아도 기본값이 클러스터 IP이기 때문이다.

그리고 이 서비스에 `9000`번 포트로 접근을 하면 서비스가 타겟이 되는 컨테이너의 `8080`포트로 연결을 해준다.

 

한번 서비스가 잘 동작하는지 확인하기 위해 `Kubernetes` 클러스터 내부의 `k8s-master`에 접속해 `curl`을 전송하자.

 

 

 

서비스와 잘 통신이 되는 것을 확인할 수 있다.

(해당 `curl` 명령어를 사용시 `Hostname`을 반환해주는 건 인프런의 일프로님의 이미지에 관련 설정이 되어있기 때문이다.)

 

 

만약, 해당 주소로 내부망인 데스크탑에서 접근을 하면 어떻게 될까?

 

 

내부망인 데스크탑이라도 `Kubernetes` 클러스터가 아니기 때문에 접근이 불가능한 것을 확인할 수 있다.

 

 

 

그럼 본론으로 돌아와 우리의 처음 목표였던 `Pod`를 삭제하고 다시 만들었을 때도 서비스와 연결이 되는지 확인해보자.

 

 

서비스의 `Pod`를 삭제시킨 뒤 재생성해보자.

 

 

 

`Pod`를 재생성해도 알아서 서비스가 라벨을 통해 인식하여 연결이 된 것을 확인할 수 있다.

 

 

 

다음으로는 `NodePort`를 만들어보자.

 

apiVersion: v1
kind: Service
metadata:
  name: svc-2
spec:
  selector:
    app: pod
  ports:
  - port: 9000
    targetPort: 8080
    nodePort: 30001
  type: NodePort

 

우선, 앞서 만들었던 Cluster IP 타입의 서비스를 삭제하고 위 YAML 파일로 서비스를 생성해보자.

 

`nodePort`는 `30001` 번으로 지정을 해주었다.

 

 

 

생성된 서비스를 보면 클러스터 IP가 자동으로 생성이 되었고 사용되는 내부 엔드포인트 포트는 2개가 할당이 되었다.

그 중 `30001`번 포트가 우리가 생성한 `nodePort`이다.

 

 

생성된 `nodePort`로 각 노드에 `curl`을 보내면 모든 노드에 적용되어 다음과 같이 응답이 잘 오는 것을 확인할 수 있다.

 

 

 

이 상태에서 `pod-2`를 하나 더 생성해보자.

 

 

apiVersion: v1
kind: Pod
metadata:
  name: pod-2
  labels:
     app: pod
spec:
  nodeSelector:
    kubernetes.io/hostname: k8s-node2
  containers:
  - name: container
    image: kubetm/app
    ports:
    - containerPort: 8080

 

 

이때는 2번 노드를 선택해주었다. 그 후, 서비스를 다시 확인하면 `Pod`가 2개 연결된 것을 확인할 수 있다.

 

 

 

 

 

그리고 `nodePort`로 접근을 시도하면 서비스가 각각이 연결되어 있는 `Pod`들한테 트래픽을 분산해서 전달을 해준다.

(위의 사진을 보면 트래픽이 분산되어 가는 것을 확인할 수 있다.)

 

 

다음으로는 아까 설명했던 `externalTrafficPolicy` 옵션을 사용했을 때의 예시이다.

 

우선, 기존의 서비스를 지우고 해당 옵션을 달아서 서비스를 다시 생성해보자.

 

apiVersion: v1
kind: Service
metadata:
  name: svc-2
spec:
  selector:
    app: pod
  ports:
  - port: 9000
    targetPort: 8080
    nodePort: 30001
  type: NodePort
  externalTrafficPolicy: Local

 

서비스를 생성한 뒤 `Node1`에 접근하면 `Node1`을 선택한 `Pod` 1번에만 접근하는 것을 볼 수 있다.

 

 

 

`Pod` 2번도 마찬가지이다.

 

 

 

그럼 만약, `Node1`에는 `Pod`가 없는데 `Node1`의 IP를 계속 호출한다면 어떻게 될까?

 

 

`Pod` 1번을 삭제한 뒤 접근하면 아래와 같이 계속 기다리고만 있게 된다.

 

 

 

따라서, `externalTrafficPolicy` 옵션을 사용할 때는 해당 부분에 유의해야 한다.

 

 

 

 

마지막으로, `LoadBalancer` 타입의 서비스를 생성해보자.

 

apiVersion: v1
kind: Service
metadata:
  name: svc-3
spec:
  selector:
    app: pod
  ports:
  - port: 9000
    targetPort: 8080
  type: LoadBalancer

 

 

사진을 보면 클러스터 IP도 정상적으로 잘 생성이 되었고 `NodePort`도 자동으로 생겼다.

하지만 계속 `Pending` 상태로 생성이 되지 않는다.

 

그 이유는 `LoadBalancer` 타입은 외부에서 접근이 가능하도록 External IP를 자동으로 할당해주는 플러그인이 있어야 한다.

하지만, 기본적으로 `Kubernetes`를 설치할 때는 그게 없기 때문에 External IP가 보이지 않는다.

 

실제로 클러스터 Master에서 아래 명령어를 입력하면,

 

다음과 같이 EXTERNAL-IP가 계속 `Pending` 상태인걸 확인할 수 있다. 이는 플러그인이 없기 때문이다.