쿠버네티스에서 반드시 알아야 할 서비스(Service) 유형

파드는 특성상 생성될 때마다 내부 IP 주소가 계속 변화하게 된다. 쿠버네티스의 서비스(Service)는 이러한 파드에 탑재된 애플리케이션이 외부와 상호 통신이 가능하도록 만들어준다. 이번 글에서는 쿠버네티스 클러스터 운영에 반드시 필요한 서비스 유형을 살펴본다.

쿠버네티스에서 반드시 알아야 할 서비스(Service) 유형

서비스(Service)란?

쿠버네티스 환경에서 서비스(Service)파드들을 통해 실행되고 있는 애플리케이션을 네트워크에 노출(expose)시키는 가상의 컴포넌트다. 쿠버네티스 내부의 다양한 객체들이 애플리케이션과, 그리고 애플리케이션이 다른 외부의 애플리케이션이나 사용자와 연결될 수 있도록 도와주는 역할을 한다.

왜 서비스란 것이 필요하게 되었을까? 클러스터 안에서 애플리케이션을 구동시키는 데에 쓰이는 파드들의 반영속적인(ephemeral) 특성 때문이다. 파드를 소개했던 지난 글의 내용처럼, 쿠버네티스에서의 파드는 무언가가 구동 중인 상태를 유지하기 위해 동원되는 일회성 자원으로 언제든 다른 노드로 옮겨지거나 삭제될 수 있다. 또한 파드는 생성될 때마다 새로운 내부 IP를 받게 되므로, 이것만으로 클러스터 내/외부와 통신을 계속 유지하기는 어렵다.

따라서 쿠버네티스에서는 파드가 외부와 통신할 수 있도록 클러스터 내부에서 고정적인 IP를 갖는 서비스(Service)를 이용하도록 하고 있다. 서비스는 또한 디플로이먼트나 스테이트풀셋처럼 같은 애플리케이션을 구동하도록 구성된 여러 파드들에게 단일한 네트워크 진입점을 부여하는 역할도 한다.

서비스를 정의하고 생성할 때에는 spec.ports 아래에 연결하고자 하는 항목 별로 각각 2개씩의 포트가 지정되어야 한다. 아래의 항목 명칭은 클러스터 안에서 트래픽을 중계하는 서비스의 입장에서 기술된 것임을 이해하자.

  • targetPort : 파드의 애플리케이션 쪽에서 열려있는 포트를 의미한다. 서비스로 들어온 트래픽은 해당 파드의 <클러스터 내부 IP>:<targetPort>로 넘어가게 된다.
  • port : 서비스 쪽에서 해당 파드를 향해 열려있는 포트를 의미한다.

서비스의 유형

쿠버네티스에서 서비스의 유형은 크게 4가지로 분류된다. 명세(spec) 상에 type가 별도로 지정되지 않았다면 ClusterIP로 고정된다.

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

1. ClusterIP (기본 형태)

ClusterIP파드들이 클러스터 내부의 다른 리소스들과 통신할 수 있도록 해주는 가상의 클러스터 전용 IP다. 이 유형의 서비스는 <ClusterIP>로 들어온 클러스터 내부 트래픽을 해당 파드의 <파드IP>:<targetPort>로 넘겨주도록 동작하므로, 오직 클러스터 내부에서만 접근 가능하게 된다. 쿠버네티스가 지원하는 기본적인 형태의 서비스다.

ClusterIP 유형의 서비스 구조 (이미지 출처 : Google Cloud) https://medium.com/google-cloud/kubernetes-nodeport-vs-loadbalancer-vs-ingress-when-should-i-use-what-922f010849e0
ClusterIP 유형의 서비스 구조 (이미지 출처 : Google Cloud)

Selector를 포함하는 형태

TCP 포트 9376을 수신 대기(listen)하며 app=myapp, type=frontend라는 레이블을 공유하는 파드들에게 myapp-service라는 이름으로 접근할 수 있게 해주는 ClusterIP 유형의 서비스를 정의하면 다음과 같다.

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  type: ClusterIP		# 생략 가능
  ports:
  - protocol: TCP
    targetPort: 9376	# 애플리케이션(파드)을 노출하는 포트
    port: 80			# 서비스를 노출하는 포트
  selector:				# 이 서비스가 적용될 파드 정보를 지정 (선택이나 권장 사항)
    app: myapp
    type: frontend

위에서 spec.selector에서 지정된 레이블로 여러 파드들이 존재할 경우, 서비스는 그 파드들을 외부 요청(request)을 전달할 엔드포인트(endpoints)로 선택하여 트래픽을 분배하게 된다. 이를 이용하여 한 노드 안에 여러 파드, 또는 여러 노드에 걸쳐 있는 여러 파드에 동일한 서비스를 적용할 수 있다.

때로는 여러 포트들의 연결이 필요할 때도 있다. (예: HTTP/HTTPS) 이럴 땐 spec.ports에 리스트 형태로 name 값을 부여하여 각각 추가해주면 된다. 이때 name 필드에 들어가는 값은 반드시 영문/소문자 및 숫자와 -로만 이루어져야 하며, 첫 글자와 마지막 글자는 반드시 영문/소문자 및 숫자로만 쓰여야 한다.

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  type: ClusterIP
  ports:
  - name: http
    protocol: TCP
    targetPort: 9376
    port: 80
  - name: https
    protocol: TCP
    targetPort: 9377
    port: 443
  selector:
    app: myapp
    type: frontend

Selector가 제외된 형태

필요에 따라 엔드포인트(Endpoints)를 수동으로 직접 지정해줘야 할 때가 있다. 테스트 환경과 상용 환경의 설정이 서로 다르거나, 다른 네임스페이스 또는 클러스터에 존재하는 파드와의 네트워크를 위해 서비스-서비스 간의 연결을 만들어야 하는 상황 등이 있다.

이런 경우에는 spec.selector 없이 서비스를 만들고, 해당 서비스가 가리킬 엔드포인트(Endpoints) 객체를 직접 만들어 해당 서비스에 맵핑하는 방법이 있다.

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376
apiVersion: v1
kind: Endpoints
metadata:
  name: myapp-service		# 연결할 서비스와 동일한 name을 메타데이터로 입력
subsets:					# 해당 서비스로 가리킬 endpoint를 명시
  - addresses:
      - ip: 192.0.2.42
    ports:
      - port: 9376

이때 주의해야 할 점은, 엔드포인트로 명시할 IP는 loopback(127.0.0.0/8) 또는 link-local(169.254.0.0/16, 224.0.0.0/24) 이어서는 안 된다는 것이다. 이에 대한 자세한 내용은 쿠버네티스 공식 메뉴얼 문서에서 확인할 수 있다.

2. NodePort

NodePort외부에서 노드 IP의 특정 포트(<NodeIP>:<NodePort>)로 들어오는 요청을 감지하여, 해당 포트와 연결된 파드로 트래픽을 전달하는 유형의 서비스다. 이때 클러스터 내부로 들어온 트래픽을 특정 파드로 연결하기 위한 ClusterIP 역시 자동으로 생성된다.

NodePort 유형의 서비스 구조 (이미지 출처 : Google Cloud) https://medium.com/google-cloud/kubernetes-nodeport-vs-loadbalancer-vs-ingress-when-should-i-use-what-922f010849e0
NodePort 유형의 서비스 구조 (이미지 출처 : Google Cloud)

이 유형의 서비스에서는 spec.ports 아래에 nodePort를 추가로 지정할 수 있다. nodePort는 외부에서 노드 안의 특정 서비스로 접근할 수 있도록 지정된 노드의 특정 포트를 의미한다. nodePort로 할당 가능한 포트 번호의 범위는 30000에서 32767 사이이며, 미지정시 해당 범위 안에서 임의로 부여된다.

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  type: NodePort
  ports:
  - targetPort: 80		# 애플리케이션(파드)을 노출하는 포트
    port: 80			# 서비스를 노출하는 포트
    nodePort: 30008		# 외부 사용자가 애플리케이션에 접근하기 위한 포트번호(선택)
  selector:				# 이 서비스가 적용될 파드 정보를 지정
    app: myapp
    type: frontend

NodePort의 경우에도 spec.selector에 해당하는 모든 파드들에 동일한 로드 밸런싱이 적용된다. 만약 같은 레이블의 파드들이 다른 여러 노드에 걸쳐 존재한다면, 해당 노드들에도 같은 서비스가 자동으로 생성되면서 같은 번호의 노드포트를 통한 해당 파드들의 접근이 허용된다. 예를 들어 192.168.1.2 노드와 192.168.1.3 노드에 각각 같은 레이블의 파드가 존재할 경우, NodePort 서비스를 통해 192.168.1.2:<nodePort> 또는 192.168.1.3:<nodePort> 중 어떤 경로를 이용하더라도 해당되는 파드들에 연결이 가능해진다.

3. LoadBalancer

별도의 외부 로드 밸런서를 제공하는 클라우드(AWS, Azure, GCP 등) 환경을 고려하여, 해당 로드 밸런서를 클러스터의 서비스로 프로비저닝할 수 있는 LoadBalancer 유형도 제공된다.

LoadBalancer 유형의 서비스 구조 (이미지 출처 : Google Cloud) https://medium.com/google-cloud/kubernetes-nodeport-vs-loadbalancer-vs-ingress-when-should-i-use-what-922f010849e0
LoadBalancer 유형의 서비스 구조 (이미지 출처 : Google Cloud)

이 유형은 서비스를 클라우드 제공자 측의 자체 로드 밸런서로 노출시키며, 이에 필요한 NodePortClusterIP 역시 자동 생성된다. 이때 프로비저닝된 로드 밸런서의 정보는 서비스의 status.loadBalancer 필드에 게재된다.

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  type: LoadBalancer
  ports:
    - protocol: TCP
      port: 80				# 서비스를 노출하는 포트
      targetPort: 80		# 애플리케이션(파드)를 노출하는 포트
  clusterIP: 10.0.171.239	# 클러스터 IP
  selector:
    app: myapp
    type: frontend
status:
  loadBalancer:				# 프로비저닝된 로드 밸런서 정보
    ingress:
    - ip: 192.0.2.127

이렇게 구성된 환경에서는, 외부의 로드 밸런서를 통해 들어온 트래픽이 서비스의 설정값을 따라 해당되는 파드들로 연결된다. 이 트래픽이 어떻게 로드 밸런싱이 될지는 클라우드 제공자의 설정에 따르게 된다.

만약 이러한 방식의 로드 밸런서 프로비저닝을 지원하지 않는 클라우드 환경(예: Virtualbox)일 경우, 이 유형으로 지정된 서비스는 NodePort와 동일한 방식으로 동작하게 된다.

4. ExternalName

서비스에 selector 대신 DNS name을 직접 명시하고자 할 때에 쓰인다. spec.externalName 항목에 필요한 DNS 주소를 기입하면, 클러스터의 DNS 서비스가 해당 주소에 대한 CNAME 레코드를 반환하게 된다.

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
  namespace: prod
spec:
  type: ExternalName
  externalName: my.database.example.com

CLI 명령어로 파드에 서비스 적용하기

서비스는 YAML 형태로 정의하는 것이 좋지만, 생성된 파드를 간단히 외부에 노출시키고자 할 때에는 CLI 명령어로 보다 간편하게 수행할 수도 있다. kubectl create service <서비스유형> ... 형태로 할 수도 있지만, 특정 리소스에 한해 즉시 노출시키고자 한다면 kubectl expose 명령을 이용하여 서비스 배포와 노출을 동시에 진행 가능하다.

# Create a new pod called custom-nginx using the nginx image and expose it on container port 8080.
kubectl run custom-nginx --image=nginx --port=8080 && kubectl expose pod custom-nginx

예를 들어 redis라는 파드의 6379 포트를 ClusterIP로 클러스터에 노출시켜야 한다고 가정해보자. 서비스를 직접 생성하여 연결하려면 아래와 같은 명령을 생각해 볼 수 있다.

kubectl create service clusterip redis --tcp=6379:6379

그러나 위와 같은 접근법에는 문제가 하나 있다. 명령어를 통한 서비스 생성시 spec.selectorapp=redis로 고정되며, 따로 YAML로 빼서 다시 수정하지 않는 이상 실제 연결하고자 하는 파드의 레이블 정보를 반영할 수 없다. 따라서 이런 경우에는 서비스를 직접 만들기보다는, 다음과 같이 kubectl expose pod 명령을 사용하는 것이 좋다.

kubectl expose pod redis --port=6379 --name redis-service

맺음말

이상으로 ClusterIP, NodePort, LoadBalancer, ExternalName의 네 종류로 구성된 쿠버네티스의 서비스 종류를 살펴보았다.

서비스는 파드 또는 파드들의 그룹을 통해 구동되는 애플리케이션에게 고정적인 네트워크 접근점을 만들어 주는 객체다. 실제 쿠버네티스 클러스터를 통해 여러 애플리케이션으로 구성된 시스템을 구축하려면, 위와 같은 서비스들의 유형과 용도를 명확히 이해하는 것이 필요하다.

참고자료