infra
Platform

모듈 맵

[Kubernetes] ClusterIP, NodePort, LoadBalancer 서비스 완전 분석

0 / 29 완료

펼치기
0 / 29 완료0%

Kubernetes · 06 / 29

[Kubernetes] ClusterIP, NodePort, LoadBalancer 서비스 완전 분석

ClusterIP, NodePort, LoadBalancer, ExternalName 네 가지 서비스 타입과 Selector로 파드를 연결하는 원리를 학습합니다

🚨INCIDENT ALERT
HIGH

파드는 정상인데 API 서버에 접속하는 프론트엔드가 계속 연결 실패를 보고합니다. 온콜 엔지니어가 ClusterIP, NodePort, LoadBalancer의 차이를 모르면 내부 통신 문제인지 외부 노출 문제인지 구분할 수 없습니다. Service는 변하는 Pod IP 위에 안정적인 네트워크 진입점을 만드는 장치입니다.

Kubernetes Service 타입 완전 정복

파드를 배포하고 나서 처음으로 마주치는 벽이 있습니다. kubectl get pods로 파드가 Running 상태임을 확인했는데, 정작 브라우저에서 접속하거나 다른 서비스에서 호출하려 하면 아무런 응답이 없습니다. 파드는 IP를 가지고 있지만, 그 IP는 재시작할 때마다 바뀌고 외부에서는 원래 접근할 수 없습니다. Kubernetes는 이 문제를 Service라는 오브젝트로 해결합니다. Service는 항상 동일한 진입점(고정 IP와 DNS 이름)을 제공하고, 뒤에 있는 파드들로 트래픽을 분산합니다. 운영 환경에서 파드 IP를 직접 사용하는 팀은 없습니다. 서비스 타입 네 가지의 용도 차이를 이해하면, 어떤 상황에서 어떤 타입을 선택해야 하는지 자연스럽게 판단할 수 있게 됩니다.


이번 챕터에서 배울 것

파드에 안정적으로 접근하기 위한 Service 오브젝트의 네 가지 타입을 이해하고, Selector를 통해 파드와 연결되는 원리를 실습합니다.

  • 1ClusterIP — 클러스터 내부 전용 가상 IP와 기본 서비스 타입
  • 2NodePort — 노드 IP + 포트로 외부 트래픽 유입
  • 3LoadBalancer — 클라우드 로드밸런서 자동 프로비저닝
  • 4ExternalName — 외부 DNS 이름을 클러스터 내부 서비스처럼 사용
  • 5Selector와 label 매칭으로 파드에 연결하는 원리
  • 6Endpoints와 kube-dns(CoreDNS) 동작 방식
실습 환경 준비

로컬 minikube나 kind 클러스터, 또는 클라우드 클러스터 모두 실습 가능합니다. 이전 챕터(deployment-basics)에서 Deployment 개념을 익혔다면 바로 진행합니다.

클러스터 접근 확인
kubectl cluster-info
노드 상태 확인
kubectl get nodes
실습용 네임스페이스 생성
kubectl create namespace svc-lab
현재 컨텍스트 확인
kubectl config current-context
💡개념

Service란 무엇인가 — 파드 앞에 놓이는 안정적인 진입점

프론트엔드 팀에서 "API 서버 IP가 또 바뀌었냐"고 문의가 왔습니다. Deployment를 재배포할 때마다 파드 IP가 바뀌는데, 클라이언트가 파드 IP를 직접 사용하고 있었던 겁니다. 파드를 스케일 아웃하면 어떤 파드로 보내야 하는지도 클라이언트가 직접 결정해야 하는 문제도 생깁니다. Service는 파드 IP 변화를 추상화하는 안정적인 진입점입니다. 파드가 죽어도, 스케일이 바뀌어도 Service의 IP와 DNS 이름은 변하지 않습니다.

파드는 언제든지 재시작될 수 있고, Deployment가 스케일 아웃하면 파드 수가 늘어납니다. 각 파드가 고유한 IP를 가지지만 이 IP는 파드가 죽으면 사라집니다. 클라이언트가 파드 IP를 직접 사용한다면, 파드가 재시작될 때마다 설정을 변경해야 합니다. Service는 이 문제를 해결합니다.

Service란 무엇인가 — 파드 앞에 놓이는 안정적인 진입점

Service의 핵심 역할

클라이언트
    │
    ▼
┌─────────────────────────────┐
│  Service (고정 ClusterIP)    │  ← 항상 동일한 IP/DNS
│  10.96.45.123               │
└──────────┬──────────────────┘
           │ 트래픽 분산 (round-robin)
    ┌──────┼──────┐
    ▼      ▼      ▼
  Pod-1  Pod-2  Pod-3   ← IP가 바뀌어도 Service는 항상 올바른 파드로 연결
  (재시작해도 Service가 알아서 추적)

Selector와 label 매칭

Service는 spec.selector에 정의된 label과 일치하는 파드를 자동으로 탐지합니다. label이 일치하는 파드가 새로 생기면 자동으로 연결에 포함되고, 파드가 죽으면 자동으로 제외됩니다.

YAML
# 파드 (Deployment의 template)
metadata:
  labels:
    app: my-web          # ← 이 label을
    version: v2

# Service
spec:
  selector:
    app: my-web          # ← 여기서 매칭. app=my-web인 모든 파드로 연결

Endpoints 오브젝트

Kubernetes는 Selector 매칭 결과를 Endpoints 오브젝트로 자동 관리합니다. Service가 생성되면 같은 이름의 Endpoints가 자동으로 만들어지고, 매칭되는 파드의 IP:Port 목록이 채워집니다.

Kubernetes
# Endpoints 확인 — Service가 실제로 어떤 파드에 연결되는지 확인
kubectl get endpoints my-service

# NAME         ENDPOINTS                          AGE
# my-service   10.244.0.5:8080,10.244.0.6:8080   5m
#              ↑ 실제 파드 IP:Port 목록
🔍실행 후 확인할 것
  • NAME조회 대상 리소스 이름이 예상한 대상과 일치하는지 확인합니다.
  • STATUS/READYRunning, Ready, Available처럼 정상 상태를 나타내는 필드가 있는지 봅니다.
  • RESTARTS/EVENTS재시작 횟수나 Warning 이벤트가 증가하지 않는지 확인합니다.

💡개념

ClusterIP — 기본 서비스 타입, 클러스터 내부 통신

마이크로서비스 아키텍처에서 Order 서비스가 Payment 서비스를 호출해야 합니다. Payment 서비스는 외부에 노출하면 안 되고, Order 서비스에서만 접근 가능해야 합니다. ClusterIP가 이 요구에 딱 맞는 서비스 타입입니다. 클러스터 내부에서만 유효한 가상 IP와 DNS 이름을 부여하고, 외부에서는 라우팅되지 않습니다. 대부분의 내부 마이크로서비스 통신은 ClusterIP 하나로 충분합니다.

ClusterIP는 type 필드를 생략했을 때 자동으로 적용되는 기본 서비스 타입입니다. 클러스터 내부에서만 유효한 가상 IP를 Service에 할당합니다. 외부에서 직접 접근할 수 없으므로, 인터넷에 노출하지 않아야 하는 내부 마이크로서비스(DB, 캐시, 내부 API)에 적합합니다.

ClusterIP — 기본 서비스 타입, 클러스터 내부 통신

ClusterIP Service 생성

YAML
# clusterip-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-web-service
  namespace: svc-lab
spec:
  type: ClusterIP        # 생략해도 동일 (기본값)
  selector:
    app: my-web          # 이 label을 가진 파드로 연결
  ports:
    - name: http
      port: 80           # Service 포트 (클라이언트가 접근하는 포트)
      targetPort: 8080   # 파드가 실제로 리스닝하는 포트
      protocol: TCP
Kubernetes
kubectl apply -f clusterip-service.yaml

# Service 확인
kubectl get svc my-web-service -n svc-lab
# NAME             TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
# my-web-service   ClusterIP   10.96.45.123   <none>        80/TCP    30s
#                              ↑ 내부 전용 IP     ↑ none = 외부 노출 없음

kube-dns로 이름 해석

CoreDNS(kube-dns)가 자동으로 DNS 이름을 생성합니다. 같은 네임스페이스에서는 서비스 이름만으로 접근 가능합니다.

로컬 터미널
# 완전한 DNS 이름 (FQDN)
my-web-service.svc-lab.svc.cluster.local

# 같은 네임스페이스에서: 서비스 이름만으로 접근
curl http://my-web-service

# 다른 네임스페이스에서: namespace 포함
curl http://my-web-service.svc-lab

# 또는 전체 FQDN 사용
curl http://my-web-service.svc-lab.svc.cluster.local
Kubernetes
# 클러스터 내부 파드에서 DNS 해석 테스트
kubectl run dns-test --image=busybox -it --rm --restart=Never -- \
  nslookup my-web-service.svc-lab
# Server:    10.96.0.10
# Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
#
# Name:      my-web-service.svc-lab
# Address 1: 10.96.45.123 my-web-service.svc-lab.svc.cluster.local

💡개념

NodePort — 노드 포트로 외부 트래픽 수신

온프레미스 클러스터나 클라우드 로드밸런서가 없는 환경에서 외부에서 서비스를 테스트해야 할 때가 있습니다. 개발 환경에서 QA 팀이 직접 API를 호출하거나, 간단한 데모 서버를 노출할 때 LoadBalancer를 프로비저닝하는 것은 부담스럽습니다. NodePort는 클러스터의 모든 노드 IP에 같은 포트를 열어 외부에서 바로 접근할 수 있게 합니다. 설정이 단순하지만 노드 IP가 직접 노출된다는 점은 프로덕션 사용 시 고려해야 합니다.

NodePort는 클러스터의 모든 노드에 동일한 포트(30000~32767)를 열어 외부 트래픽을 받습니다. <NodeIP>:<NodePort>로 접근하면 Service를 거쳐 파드로 전달됩니다. 클라우드가 아닌 온프레미스 환경이나 개발/테스트 용도로 자주 사용합니다. 프로덕션에서는 노드 IP가 직접 노출되고 포트 범위가 제한적이어서 LoadBalancer나 Ingress와 함께 사용합니다.

NodePort Service 생성

YAML
# nodeport-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-web-nodeport
  namespace: svc-lab
spec:
  type: NodePort
  selector:
    app: my-web
  ports:
    - name: http
      port: 80           # ClusterIP 포트 (클러스터 내부 접근 시)
      targetPort: 8080   # 파드 포트
      nodePort: 30080    # 노드에 열리는 포트 (생략 시 자동 할당)
      protocol: TCP
Kubernetes
kubectl apply -f nodeport-service.yaml

kubectl get svc my-web-nodeport -n svc-lab
# NAME               TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
# my-web-nodeport    NodePort   10.96.55.234    <none>        80:30080/TCP   10s
#                                                             ↑  ↑
#                                                    ClusterIP  NodePort

# 노드 IP 확인
kubectl get nodes -o wide
# NAME       STATUS   ...   INTERNAL-IP    EXTERNAL-IP
# node-1     Ready    ...   192.168.49.2   <none>

# 외부에서 접근 (노드 IP + NodePort)
curl http://192.168.49.2:30080

# minikube 환경에서 자동으로 URL 얻기
minikube service my-web-nodeport -n svc-lab --url

트래픽 흐름

외부 클라이언트
    │
    ▼
노드IP:30080 (모든 노드에서 동일하게 수신)
    │
    ▼
ClusterIP:80 (iptables/IPVS 규칙)
    │
    ▼
파드IP:8080

💡개념

LoadBalancer와 ExternalName — 클라우드 연동과 외부 서비스 추상화

EKS나 GKE 같은 관리형 클러스터에서 서비스를 인터넷에 공개해야 할 때, 각 서비스마다 LoadBalancer를 만들면 클라우드 비용이 서비스 수만큼 증가합니다. 또한 외부에서 운영하는 레거시 데이터베이스를 이전하는 중이라면, 코드 변경 없이 목적지를 바꿀 수 있는 추상화가 필요합니다. LoadBalancer는 클라우드 제공자의 로드밸런서를 자동으로 프로비저닝해 공인 IP를 할당하고, ExternalName은 외부 DNS 이름을 클러스터 내부 서비스처럼 참조할 수 있게 합니다.

LoadBalancer — 클라우드 로드밸런서 자동 프로비저닝

LoadBalancer 타입은 클라우드 제공자(AWS ELB, GCP Cloud Load Balancing, Azure Load Balancer)의 로드밸런서를 자동으로 생성하고 공인 IP를 할당합니다. 내부적으로 NodePort를 포함하며, 클라우드 컨트롤러가 실제 로드밸런서와 연동합니다.

YAML
# loadbalancer-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-web-lb
  namespace: svc-lab
spec:
  type: LoadBalancer
  selector:
    app: my-web
  ports:
    - port: 80
      targetPort: 8080
Kubernetes
kubectl apply -f loadbalancer-service.yaml

# EXTERNAL-IP에 공인 IP가 할당될 때까지 대기
kubectl get svc my-web-lb -n svc-lab -w
# NAME          TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)        AGE
# my-web-lb     LoadBalancer   10.96.60.100    <pending>       80:31234/TCP   10s
# my-web-lb     LoadBalancer   10.96.60.100    203.0.113.10    80:31234/TCP   45s
#                                              ↑ 공인 IP 할당됨

# 공인 IP로 접근
curl http://203.0.113.10

minikube에서는 minikube tunnel 명령어를 먼저 실행해야 EXTERNAL-IP가 할당됩니다.

ExternalName — 외부 서비스를 클러스터 내부처럼 사용

ExternalName은 외부 DNS 이름을 클러스터 내부 서비스 이름으로 매핑합니다. Selector 없이 CNAME 레코드만 생성하며, 외부 데이터베이스나 SaaS API를 클러스터 내부 서비스처럼 참조할 때 유용합니다.

YAML
# externalname-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: external-db
  namespace: svc-lab
spec:
  type: ExternalName
  externalName: my-database.example.com   # 외부 DNS 이름
  # selector 없음
Kubernetes
kubectl apply -f externalname-service.yaml

# 이제 클러스터 내부에서 external-db로 접근하면
# my-database.example.com으로 CNAME 해석됨
curl http://external-db.svc-lab.svc.cluster.local

서비스 타입 비교

타입외부 접근용도클라우드 필요
ClusterIP불가내부 마이크로서비스불필요
NodePort가능 (노드IP:포트)개발/온프레미스불필요
LoadBalancer가능 (공인IP)프로덕션 외부 노출필요
ExternalName해당 없음외부 서비스 추상화불필요

💡개념

전체 실습 — Deployment + Service 연동

Service 타입을 하나씩 배운 것만으로는 실제 Deployment와 어떻게 연결되는지 체감하기 어렵습니다. Selector가 잘못되거나 label이 안 맞으면 Service는 생성되어도 파드로 트래픽이 가지 않습니다. 이 실습에서는 Deployment와 ClusterIP Service를 함께 배포하고, Endpoints 오브젝트를 통해 실제로 파드와 연결됐는지 확인합니다. 이 흐름을 한 번 직접 경험하면 이후 Selector 불일치 문제를 빠르게 진단할 수 있게 됩니다.

실제 운영과 동일한 방식으로 Deployment와 Service를 함께 배포합니다.

YAML
# full-example.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-web
  namespace: svc-lab
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-web
  template:
    metadata:
      labels:
        app: my-web        # Service Selector와 반드시 일치해야 함
        version: v1
    spec:
      containers:
        - name: web
          image: nginx:alpine
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: my-web-service
  namespace: svc-lab
spec:
  type: ClusterIP
  selector:
    app: my-web            # Deployment label과 일치
  ports:
    - port: 80
      targetPort: 80
Kubernetes
# 배포
kubectl apply -f full-example.yaml

# 파드와 서비스 확인
kubectl get pods,svc -n svc-lab
# NAME                          READY   STATUS    ...
# pod/my-web-7d9f4c8b9-4xk2p   1/1     Running   ...
# pod/my-web-7d9f4c8b9-8qw3r   1/1     Running   ...
# pod/my-web-7d9f4c8b9-mp9ts   1/1     Running   ...
#
# NAME                   TYPE        CLUSTER-IP      PORT(S)
# service/my-web-service ClusterIP   10.96.45.123    80/TCP

# Endpoints 확인 — 3개 파드 IP가 모두 등록되어 있어야 함
kubectl get endpoints my-web-service -n svc-lab
# NAME               ENDPOINTS
# my-web-service     10.244.0.5:80,10.244.0.6:80,10.244.0.7:80

# 클러스터 내부에서 서비스 접근 테스트
kubectl run curl-test --image=curlimages/curl -it --rm --restart=Never \
  -n svc-lab -- curl http://my-web-service
# <!DOCTYPE html>  ← Nginx 기본 페이지 응답

# 서비스를 통해 로드밸런싱 확인 (여러 번 호출)
kubectl run curl-test --image=curlimages/curl -it --rm --restart=Never \
  -n svc-lab -- sh -c 'for i in $(seq 1 5); do curl -s http://my-web-service/hostname; echo; done'

문제 상황

Kubernetes
kubectl get svc my-web-service -n svc-lab
# NAME             TYPE        CLUSTER-IP     PORT(S)   AGE
# my-web-service   ClusterIP   10.96.45.123   80/TCP    2m  ← 서비스는 존재

kubectl run curl-test --image=curlimages/curl -it --rm --restart=Never \
  -n svc-lab -- curl http://my-web-service
# curl: (7) Failed to connect to my-web-service port 80: Connection refused

서비스가 생성되고 파드도 Running인데 연결이 되지 않습니다.

진단 1: Endpoints 확인 — Selector 불일치 판별

Kubernetes
kubectl get endpoints my-web-service -n svc-lab
# NAME               ENDPOINTS   AGE
# my-web-service     <none>      2m
#                    ↑ 빈 값 = Selector와 일치하는 파드가 없음

Endpoints가 <none>이면 Selector 불일치가 원인입니다.

진단 2: 파드 label 확인

Kubernetes
# 파드에 실제로 붙어 있는 label 확인
kubectl get pods -n svc-lab --show-labels
# NAME                    READY   STATUS    LABELS
# my-web-7d9f4c8b9-4xk2p  1/1     Running   app=my-webapp,version=v1
#                                            ↑ 'my-webapp'

진단 3: Service Selector 확인

Kubernetes
kubectl describe svc my-web-service -n svc-lab | grep Selector
# Selector: app=my-web
#           ↑ 'my-web'  ← 파드 label(my-webapp)과 불일치!

해결: label 또는 Selector 수정

방법 1: Service Selector 수정 (파드 label에 맞춤)

YAML
# service 수정
spec:
  selector:
    app: my-webapp       # 파드 label과 일치하도록 수정

방법 2: Deployment label 수정 (Service Selector에 맞춤)

YAML
# deployment template label 수정
template:
  metadata:
    labels:
      app: my-web        # Service Selector와 일치하도록 수정
Kubernetes
# 수정 적용 후 Endpoints 재확인
kubectl apply -f fixed-service.yaml
kubectl get endpoints my-web-service -n svc-lab
# NAME               ENDPOINTS
# my-web-service     10.244.0.5:80,10.244.0.6:80,10.244.0.7:80  ← 정상!

체크리스트

Kubernetes
# 1. Endpoints 확인 (가장 먼저)
kubectl get endpoints <service-name> -n <namespace>

# 2. 파드 label 확인
kubectl get pods -n <namespace> --show-labels

# 3. Service Selector 확인
kubectl get svc <service-name> -n <namespace> -o yaml | grep -A5 selector

# 4. targetPort와 파드 containerPort 일치 여부 확인
kubectl describe svc <service-name> -n <namespace>
kubectl describe pod <pod-name> -n <namespace> | grep -A3 Ports

# 5. 파드 자체가 정상적으로 리스닝하는지 확인
kubectl exec -it <pod-name> -n <namespace> -- netstat -tlnp
# 또는
kubectl exec -it <pod-name> -n <namespace> -- ss -tlnp

💼
실무 맥락
현업 패턴

실무에서 서비스 타입 선택하는 기준

현업에서 쿠버네티스 클러스터를 운영할 때 서비스 타입은 트래픽의 방향과 환경에 따라 결정합니다.

내부 마이크로서비스 연결: 항상 ClusterIP

YAML
# 예: Order 서비스가 Payment 서비스를 호출하는 경우
# Payment 서비스는 외부에 노출할 이유가 없음
apiVersion: v1
kind: Service
metadata:
  name: payment-service
spec:
  type: ClusterIP          # 외부 노출 없음
  selector:
    app: payment
  ports:
    - port: 8080
      targetPort: 8080

외부 노출 진입점: LoadBalancer 대신 Ingress 권장

실무 패턴:
  ClusterIP Service (각 마이크로서비스)
       ↑
  Ingress Controller (경로 기반 라우팅, TLS 종단)
       ↑
  LoadBalancer Service (Ingress Controller 앞에 하나만)
       ↑
  인터넷 트래픽

각 서비스마다 LoadBalancer를 생성하면 클라우드 과금이 서비스 수만큼 비례해서 늘어납니다. Ingress 하나로 여러 서비스를 라우팅하는 패턴이 표준입니다.

레거시 외부 DB 마이그레이션 중: ExternalName 활용

YAML
# 데이터베이스를 클러스터 외부에서 운영 중인 경우
apiVersion: v1
kind: Service
metadata:
  name: postgres-external
spec:
  type: ExternalName
  externalName: prod-db.internal.company.com
# 코드는 postgres-external:5432로 연결
# 실제 DB를 클러스터로 이전할 때 ExternalName만 제거하면 됨

Service 이름을 코드에서 환경변수로 참조

Python
# Python 예시 — 하드코딩 대신 환경변수 사용
import os
DB_HOST = os.getenv('DB_SERVICE_HOST', 'postgres-service')
DB_PORT = int(os.getenv('DB_SERVICE_PORT', '5432'))

Kubernetes는 Service 이름과 포트를 자동으로 환경변수로 주입합니다. (<SERVICENAME>_SERVICE_HOST, <SERVICENAME>_SERVICE_PORT)


정리

Kubernetes
# 서비스 타입별 생성 빠른 참고

# ClusterIP (기본, 내부 전용)
kubectl expose deployment my-web --port=80 --target-port=8080

# NodePort
kubectl expose deployment my-web --type=NodePort --port=80 --target-port=8080

# LoadBalancer (클라우드 환경)
kubectl expose deployment my-web --type=LoadBalancer --port=80 --target-port=8080

# 서비스 목록 확인
kubectl get svc -n svc-lab

# Endpoints 확인 (Selector 매칭 결과)
kubectl get endpoints -n svc-lab

# 서비스 상세 정보
kubectl describe svc my-web-service -n svc-lab

# 서비스 삭제
kubectl delete svc my-web-service -n svc-lab

다음 챕터에서는 Ingress Controller로 경로 기반 라우팅과 TLS를 처리하는 방법을 학습합니다.

지식 확인

퀴즈 — 4문제

Q1

ClusterIP 서비스에 대한 설명으로 올바른 것은?

Q2

Service Selector와 파드 label이 일치하지 않을 때 발생하는 현상은?

Q3

NodePort 서비스의 기본 포트 범위는?

Q4

kube-dns(CoreDNS)가 서비스 이름 해석에 사용하는 기본 형식은?

0 / 4 답변

🧪 실습으로 확인하기

K8s 기초 — Pod/Deployment/Service 생성

초급

kubectl로 nginx Pod를 생성하고 Deployment와 Service를 차례로 만들어 클러스터 외부에서 접근 가능한 상태까지 구성한다. K8s 3대 리소스의 역할과 관계를 직접 손으로 익힌다.

40📋 5단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

kubernetes중급 · 55
[Kubernetes] ConfigMap과 Secret을 이용한 코드와 환경 설정 분리
Kubernetes 트랙 계속
docker입문 · 30
[Docker] 백엔드 개발자에게 Docker와 컨테이너 가상화가 필수인 이유
Docker 트랙 시작점