포스트

Kubernetes (18) - Service로 Pod 접근 지점과 로드 밸런싱 구성하기

Kubernetes (18) - Service로 Pod 접근 지점과 로드 밸런싱 구성하기

1. Service가 필요한 이유

Deployment가 관리하는 Pod는 교체·확장·축소 과정에서 IP 주소가 바뀔 수 있다. 클라이언트가 특정 Pod IP를 직접 사용하면 Pod가 재생성됐을 때 통신 대상이 사라지고, 여러 Pod에 트래픽을 나누기도 어렵다.

Service는 같은 기능을 제공하는 Pod 집합 앞에 고정된 접근 지점을 제공하는 Kubernetes 리소스다. 클라이언트는 Service의 이름 또는 가상 IP로 접속하고, Service는 selector와 일치하는 준비된 Pod 엔드포인트로 트래픽을 전달한다.

1
2
3
4
5
클라이언트
    ↓ Service 이름 또는 IP
Service (고정된 접근 지점)
    ↓ selector: app=webui
Pod 1 / Pod 2 / Pod 3

Service는 Pod 개수가 늘거나 줄어도 selector와 일치하는 엔드포인트 목록을 갱신한다. 따라서 클라이언트는 Pod IP 변화와 무관하게 같은 Service 주소를 계속 사용할 수 있다.


2. Service의 핵심 구성 요소

Service는 주로 selectorports로 어떤 Pod의 어느 포트로 보낼지 정한다.

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
  name: clusterip-service
spec:
  type: ClusterIP
  selector:
    app: webui
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

selectorapp: webui는 Pod의 레이블과 정확히 일치해야 한다. 이 예제에서는 Deployment가 만든 app=webui Pod들이 Service의 백엔드가 된다. selector가 일치하지 않으면 Service는 만들 수 있어도 연결할 엔드포인트가 없어 요청을 전달하지 못한다.

필드역할예제 값
typeService를 노출하는 방식ClusterIP
selector백엔드 Pod를 선택하는 레이블 조건app: webui
portService가 받는 포트80
targetPort선택된 Pod 컨테이너가 실제로 받는 포트80
nodePortNodePort·LoadBalancer에서 노드에 여는 포트30200

porttargetPort는 같을 필요가 없다. 예를 들어 Service의 port: 80 요청을 애플리케이션 컨테이너의 targetPort: 8080으로 전달할 수 있다.


3. Service가 Pod를 찾고 트래픽을 전달하는 과정

Service를 생성하면 Kubernetes는 selector에 맞는 Pod를 찾아 EndpointSlice에 등록한다. 실습에서 kubectl describe svc clusterip-service를 실행했을 때 다음처럼 세 개의 NGINX Pod IP가 Endpoints에 표시됐다.

1
2
Selector:   app=webui
Endpoints:  10.244.3.48:80,10.244.1.41:80,10.244.2.34:80

각 Pod의 index.htmlwebui #1, webui #2, webui #3으로 바꾼 뒤 Service IP에 반복해서 curl을 실행하자 서로 다른 응답이 반환됐다. 이는 Service가 하나의 접근 지점 뒤에서 여러 엔드포인트로 연결을 분산한다는 것을 보여 준다.

1
2
3
curl Service-IP → webui #2
curl Service-IP → webui #1
curl Service-IP → webui #3

다만 Service는 요청마다 반드시 순서대로 Pod를 하나씩 선택하는 라운드 로빈을 보장하지 않는다. 네트워크 프록시 방식과 연결 상태에 따라 같은 Pod 응답이 연속으로 나타날 수 있다.

Deployment를 3개에서 5개로 확장한 뒤 다시 describe하면 기존 엔드포인트 뒤에 + 2 more...가 표시된다. 즉 Service의 주소는 유지하면서 백엔드 목록만 자동으로 갱신된다.

1
2
3
kubectl describe svc clusterip-service
kubectl scale deploy webui --replicas=5
kubectl describe svc clusterip-service

4. ClusterIP: 클러스터 내부의 기본 접근 방식

ClusterIP는 Service 타입을 생략했을 때 적용되는 기본값이다. Service 전용 CIDR에서 가상 IP를 할당하며, 일반적으로 클러스터 내부에서만 이 IP와 DNS 이름으로 접근한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# clusterip-nginx.yaml
apiVersion: v1
kind: Service
metadata:
  name: clusterip-service
spec:
  type: ClusterIP
  # clusterIP: 10.100.100.100
  selector:
    app: webui
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

clusterIP를 생략하면 Kubernetes가 Service CIDR에서 사용 가능한 IP를 자동 할당한다. 실습에서는 10.96.213.208이 할당됐고, 이 주소로 curl해 NGINX 응답을 확인했다.

1
2
3
4
kubectl apply -f clusterip-nginx.yaml
kubectl get svc clusterip-service
kubectl describe svc clusterip-service
curl <cluster-ip>

특정 clusterIP를 직접 지정할 수도 있지만, 반드시 API 서버에 설정된 Service CIDR 범위 안의 미사용 주소여야 한다. 실습에서 10.100.100.100을 지정했을 때 현재 클러스터의 범위와 맞지 않아 다음 오류가 발생했다.

1
failed to allocate IP 10.100.100.100: the provided network does not match the current range

따라서 별도의 고정 IP 요구 사항이 없다면 자동 할당을 사용하는 편이 안전하다. 10.96.0.0/12처럼 보이는 범위는 kubeadm 등의 설치 설정에 따라 달라질 수 있으므로, 다른 클러스터의 예제 IP를 그대로 사용하면 안 된다.

클러스터 내부 Pod에서는 IP 대신 DNS 이름을 사용한다. 같은 네임스페이스라면 clusterip-service, 전체 이름이 필요하면 clusterip-service.default.svc.cluster.local처럼 접근할 수 있다.


5. NodePort: 노드 IP와 고정 포트로 노출하기

NodePort는 ClusterIP를 만들고, 모든 노드의 IP에 같은 포트를 열어 외부에서 접근할 수 있게 한다. 기본 포트 범위는 30000-32767이며, 실습의 30200은 그 범위에 포함된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# nodeport-nginx.yaml
apiVersion: v1
kind: Service
metadata:
  name: nodeport-service
spec:
  type: NodePort
  # clusterIP: 10.100.100.200
  selector:
    app: webui
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
      nodePort: 30200

접속 주소는 Pod IP가 아니라 노드 IP다.

1
http://<node-ip>:30200

실습에서 curl 10.244.3.48:30200이 실패한 것은 10.244.3.48이 Pod IP이기 때문이다. Pod의 NGINX 포트에는 curl 10.244.3.48:80으로 직접 연결할 수 있지만, 30200은 Pod에 열린 포트가 아니라 NodePort를 위해 노드에 예약된 포트다.

1
2
3
4
5
kubectl apply -f nodeport-nginx.yaml
kubectl get svc nodeport-service

# 노드 IP로 접속
curl http://<node-ip>:30200

<node-ip>:30200으로 들어온 요청도 selector에 연결된 여러 Pod 엔드포인트 중 하나로 전달되므로, 결과적으로 Service 수준의 L4 트래픽 분산이 동작한다. 다만 LoadBalancer 타입처럼 클라우드나 별도 장비의 외부 로드 밸런서가 처리하는 것은 아니다. NodePort에서는 각 노드의 kube-proxy 또는 CNI가 노드 포트로 들어온 트래픽을 Service의 백엔드 Pod로 전달한다.

nodePort를 직접 지정하면 다른 Service와 포트가 충돌하지 않는지 관리해야 한다. 생략하면 Kubernetes가 설정된 NodePort 범위에서 자동 할당한다.


6. LoadBalancer: 외부 로드 밸런서 연동

LoadBalancer는 클라우드 제공자 또는 로드 밸런서 구현체에 외부 로드 밸런서 생성을 요청하는 Service 타입이다. 일반적으로 LoadBalancer Service는 NodePort를 바탕으로 외부 로드 밸런서의 트래픽을 백엔드 Pod로 전달한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
# loadbalancer-nginx.yaml
apiVersion: v1
kind: Service
metadata:
  name: loadbalancer-service
spec:
  type: LoadBalancer
  selector:
    app: webui
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
1
2
kubectl apply -f loadbalancer-nginx.yaml
kubectl get svc loadbalancer-service

실습 환경에서는 다음처럼 EXTERNAL-IP<pending>으로 표시됐다.

1
2
NAME                   TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)
loadbalancer-service   LoadBalancer   10.96.173.134   <pending>     80:31889/TCP

이는 Kubernetes 자체에 외부 로드 밸런서 장비를 만드는 기능이 없고, 이를 수행할 클라우드 연동 또는 별도의 로드 밸런서 컨트롤러가 실습 클러스터에 없기 때문이다. 지원되는 클라우드 환경에서는 프로비저닝이 완료되면 EXTERNAL-IP 또는 호스트 이름이 할당된다.

LoadBalancer는 여러 서버로 요청을 나눠 보내는 클러스터 바깥쪽의 실제 네트워크 장비 또는 클라우드 서비스다. type: LoadBalancer는 Kubernetes가 이 인프라에 외부용 로드 밸런서를 만들어 달라고 요청하는 방식이며, YAML만으로 로드 밸런서 장비가 새로 구현되는 것은 아니다.

1
2
3
4
5
6
7
사용자
    ↓ 공인 IP 또는 도메인
외부 Load Balancer
    ↓
Kubernetes NodePort
    ↓
Service의 백엔드 Pod들

일반적인 구현에서는 외부 Load Balancer가 노드의 NodePort로 트래픽을 전달하고, NodePort Service가 다시 준비된 Pod 엔드포인트 중 하나로 보낸다. 그래서 LoadBalancer Service를 만들면 실습 출력처럼 80:31889/TCP처럼 NodePort도 함께 할당되는 경우가 많다.

구분NodePortLoadBalancer
외부 진입 주소<node-ip>:<nodePort>공인 IP 또는 외부 도메인
외부 로드 밸런서 장비없음클라우드 또는 별도 구현체가 제공
적합한 환경실습·개발·자체 로드 밸런서 구성클라우드 운영 환경

따라서 EXTERNAL-IP: <pending>은 Service 설정 오류가 아니라, 공인 IP와 외부 로드 밸런서를 프로비저닝할 인프라가 아직 없거나 준비 중이라는 의미다.

프로비저닝이 완료되면 EXTERNAL-IP에는 외부 Load Balancer에 할당된 공인 IP가 표시되는 것이 일반적이다. 제공자에 따라 숫자 IP 대신 외부 Load Balancer의 DNS 이름이 표시될 수도 있으며, 사용자는 이 주소로 접속한다.

1
EXTERNAL-IP: 203.0.113.10      # 외부 Load Balancer의 공인 IP 예시

일반적인 NodePort 기반 구현에서는 외부 Load Balancer가 정상 상태인 노드 하나를 먼저 선택해 그 노드의 NodePort로 요청을 전달한다. 이후 Service 규칙이 준비된 Pod 중 하나를 다시 선택하므로, 요청을 받은 노드와 실제 응답 Pod가 있는 노드는 다를 수 있다.

1
사용자 → Load Balancer 공인 IP → Node 1:NodePort → Pod 1·Pod 2·Pod 3 중 하나

클라우드 제공자와 설정에 따라서는 외부 Load Balancer가 NodePort를 거치지 않고 Pod IP를 직접 백엔드로 선택하는 구성도 가능하다. 어느 방식인지는 사용 중인 클라우드와 LoadBalancer 구현체의 설정에 따라 달라진다.


7. ExternalName: 외부 도메인의 DNS 별칭 만들기

ExternalName은 Pod를 선택하거나 프록시를 구성하지 않는다. 대신 클러스터 내부의 Service DNS 이름을 외부 도메인의 CNAME 별칭으로 응답하게 한다.

1
2
3
4
5
6
7
8
# external-name.yaml
apiVersion: v1
kind: Service
metadata:
  name: externalname-svc
spec:
  type: ExternalName
  externalName: google.com

이 Service를 만들면 내부 클라이언트는 externalname-svc.default.svc.cluster.local로 요청할 수 있고, 클러스터 DNS가 이를 google.com으로 해석한다.

1
2
3
4
5
6
kubectl apply -f external-name.yaml
kubectl get svc externalname-svc

# 클러스터 내부에서 확인
kubectl run testpod -it --image=centos:7 -- /bin/bash
curl externalname-svc.default.svc.cluster.local

실습 출력에서 EXTERNAL-IP 열에 google.com이 보이고, 테스트 Pod의 curl이 Google의 응답을 받은 것은 DNS 별칭이 동작했음을 보여 준다. 다만 HTTP·HTTPS에서는 내부 Service 이름으로 보낸 Host 헤더나 TLS 인증서 이름이 외부 도메인과 달라 404 또는 인증서 오류가 생길 수 있다. 실습의 Google 404도 이 점을 보여 주는 사례다.


8. Service 타입 비교와 운영 시 주의점

타입접근 주소외부 노출주요 용도
ClusterIPService DNS·Cluster IP기본적으로 불가클러스터 내부 API·DB
NodePort<node-ip>:<nodePort>가능개발·테스트, 자체 로드 밸런서의 백엔드
LoadBalancer외부 IP 또는 호스트 이름가능클라우드 환경의 외부 서비스 노출
ExternalNameService DNS → 외부 도메인 CNAME외부 도메인으로 연결외부 API·DB 도메인을 내부 이름으로 추상화

Headless Service는 네 번째 타입에 더해지는 별도 타입이 아니다. type: ClusterIP에서 clusterIP: None을 설정하는 변형으로, 하나의 가상 IP로 로드 밸런싱하지 않고 Pod별 DNS 레코드를 제공한다. StatefulSet의 Pod별 DNS에서 이 방식을 사용한다.

Service 상태와 엔드포인트는 다음 명령으로 확인한다.

1
2
3
kubectl get svc
kubectl describe svc <service-name>
kubectl get endpointslice -l kubernetes.io/service-name=<service-name>

마지막으로 kubectl delete svc --all은 현재 네임스페이스의 Service를 전부 삭제한다. 실습 로그처럼 기본 kubernetes Service까지 대상이 될 수 있으므로, 운영 환경에서는 kubectl delete svc <service-name>처럼 삭제 대상을 명확히 지정해야 한다.


9. 정리

Service는 동적으로 바뀌는 Pod 집합 앞에 고정된 이름과 접근 지점을 제공하고, selector에 맞는 준비된 Pod 엔드포인트로 트래픽을 전달한다. port는 Service의 포트, targetPort는 컨테이너 포트이며, 레이블과 selector가 일치해야 한다.

내부 통신에는 기본값인 ClusterIP를 사용하고, 간단한 외부 노출은 NodePort, 클라우드 로드 밸런서 연동은 LoadBalancer를 선택한다. ExternalName은 트래픽을 프록시하지 않고 DNS CNAME만 만들므로 HTTP·TLS 호스트 이름 문제를 고려해야 한다. Pod IP가 아닌 Service DNS 이름을 기준으로 통신하도록 설계하면 확장과 교체에도 안정적인 네트워크 구성을 유지할 수 있다.


참고 자료

  • [ServiceKubernetes 공식 문서](https://kubernetes.io/docs/concepts/services-networking/service/)
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.