infra
Platform

모듈 맵

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

0 / 29 완료

펼치기
0 / 29 완료0%

쿠버네티스 & GitOps · 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와 셀렉터 — 파드는 죽고 재생성되며 IP가 바뀌지만, Service는 고정된 이름·ClusterIP로 안정적 진입점을 제공. label selector(app=web 등)로 대상 파드를 동적으로 추적해, 파드가 교체돼도 트래픽을 올바른 파드 집합으로 전달. 파드 앞에 놓이는 안정적 추상화 계층확대

Service의 핵심 역할

ClusterIP Service — 클라이언트가 고정 ClusterIP(10.96.45.123)로 접근하면 Service가 round-robin으로 Pod-1/2/3에 분산, Pod IP가 바뀌어도 셀렉터로 추적확대

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 목록
🔍실행 후 확인할 것
  • kubectl get service에서 EXTERNAL-IP 열 먼저 확인 — LoadBalancer 타입이 <pending>이면 클라우드 제공자 연동 미설정 또는 로컬 환경(minikube에서는 minikube tunnel 실행 필요)
  • kubectl get endpoints <service-name>에서 ENDPOINTS 열 확인 — 비어있으면 Service selector가 실제 파드 레이블과 불일치. kubectl get pods --show-labels로 레이블 대조
  • ENDPOINTS에 파드 IP가 있어도 curl이 실패하면 → 파드 포트 번호가 Service의 targetPort와 다른 것. kubectl describe service <name>에서 TargetPort 항목을 파드의 실제 containerPort와 대조

Service로 온 요청이 Pod에 닿는 법 — kube-proxy와 Endpoints

💡개념

Service로 온 요청이 Pod에 닿는 법 — kube-proxy와 Endpoints 6단계

curl http://my-service 한 줄, 또는 10.96.45.123:80으로 요청을 보냅니다. 그런데 이 ClusterIP는 어느 노드에도, 어느 파드에도 붙어 있지 않은 가상 IP입니다 — 정작 그 주소로 ping을 때리면 응답조차 없습니다. 그런데도 요청은 살아 있는 파드에 정확히 도착합니다. 이 사이에서 DNS 해석 → 가상 IP 진입 → kube-proxy 규칙 → Endpoints 선택 → DNAT → 파드 전달이 순서대로 일어납니다. 이 흐름을 알면 "서비스는 있는데 왜 연결이 안 되지"를 단계로 좁혀 진단할 수 있습니다.

TEXT
[클라이언트 Pod]  curl http://my-service      (→ 10.96.45.123:80)
   │
   ① DNS 해석: my-service → ClusterIP 10.96.45.123   (CoreDNS가 응답)
   │
   ② ClusterIP:80 으로 패킷 전송 — 이 IP엔 리스닝 프로세스가 없음(가상 IP)
   │
   ③ 노드 커널의 kube-proxy 규칙(iptables/IPVS)이 그 패킷을 가로챔
   │
   ④ Endpoints 목록에서 대상 Pod 하나 선택 (round-robin/랜덤)
   │    → Endpoints = selector 매칭 + readiness 통과한 Pod IP만 등록됨
   │
   ⑤ DNAT: 목적지를 10.96.45.123:80 → 10.244.0.6:8080 으로 바꿔 씀
   │
   ⑥ 선택된 Pod로 전달 → 응답은 conntrack이 역방향 NAT로 되돌림
   ▼
[대상 Pod]  10.244.0.6:8080 에서 수신·응답

각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:

단계하는 일여기서 막히면
① DNSCoreDNS가 my-service.<namespace>.svc.cluster.local을 ClusterIP로 해석CoreDNS 장애·이름 오타 → Could not resolve host, nslookup 실패
② 가상 IPClusterIP(10.96.x.x)는 어느 NIC에도 없는 가상 주소 — 실제 서버가 리스닝하지 않음ping ClusterIP는 원래 무응답이 정상 — ping으로 진단하지 말 것
③ kube-proxy각 노드의 kube-proxy가 Service→Endpoints 매핑을 iptables/IPVS 규칙으로 심어 둠kube-proxy 미기동·규칙 누락 → 특정 노드에서만 접속 실패
④ Endpoints 선택selector와 매칭되고 Ready인 Pod IP 목록에서 하나 선택 (kubectl get endpoints로 확인)Endpoints가 <none> → selector 불일치 또는 Pod NotReady → connection refused·timeout
⑤ DNAT목적지 주소를 ClusterIP:port → PodIP:targetPort로 변환targetPort가 컨테이너 실제 포트와 다르면 DNAT는 되지만 Pod가 거부(connection refused)
⑥ 전달·응답Pod로 라우팅하고 응답은 역방향 NAT. NetworkPolicy가 있으면 이 구간에서 검사NetworkPolicy가 트래픽을 막으면 패킷 미도달, timeout

타입이 달라도 ③~⑥은 똑같습니다 — ClusterIP는 위 흐름 그대로 클러스터 내부 전용이고, NodePort는 모든 노드의 <NodeIP>:30000~32767 에 규칙을 하나 더 얹어 외부 패킷을 ⑤ DNAT로 밀어 넣으며, LoadBalancer는 그 NodePort들 앞에 클라우드 로드밸런서를 세울 뿐입니다. 즉 외부 노출 타입이 바뀌어도 "Endpoints에서 파드를 골라 DNAT" 하는 핵심 기계는 공통이고, 진입점만 달라집니다.

"Service 접속 성공"은 세 가지가 모두 참이라는 뜻입니다 — 이름이 ClusterIP로 풀렸고(①), Endpoints에 Ready 파드가 하나 이상 있고(④), targetPort가 파드의 실제 포트와 맞다(⑤). 이 중 Endpoints가 비어 있으면(④) 그 아래 단계는 볼 것도 없이 실패하므로, 연결이 안 될 때 가장 먼저 kubectl get endpoints <service>로 ④를 확인하는 것이 진단의 출발점입니다.


💡개념

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

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

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

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

트래픽 흐름

NodePort 트래픽 경로 — 외부 클라이언트→노드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(모든 노드의 30000~32767 포트로 외부 노출)·LoadBalancer(클라우드 LB 자동 프로비저닝, 외부 IP)·ExternalName(외부 도메인을 클러스터 내부 이름으로 매핑). 외부 노출 범위와 클라우드 의존성에 따라 선택확대

타입외부 접근용도클라우드 필요
ClusterIP불가내부 마이크로서비스불필요
NodePort가능 (노드IP:포트)개발/온프레미스불필요
LoadBalancer가능 (공인IP)프로덕션 외부 노출필요
ExternalName해당 없음외부 서비스 추상화불필요
Kubernetes Service 타입 선택 기준
같은 클러스터 내 다른 파드가 이 서비스를 호출해야 할 때 (기본 케이스)ClusterIP클러스터 내부 DNS로 이름 기반 접근, 외부 노출 없음, 기본값
개발 중 특정 서비스를 노드 IP로 직접 테스트해야 할 때NodePort노드 IP + 포트로 외부 접근 가능, 30000-32767 범위 포트 사용, 프로덕션 비권장
AWS/GCP/Azure 클러스터에서 외부 트래픽을 서비스로 받아야 할 때LoadBalancer클라우드 로드밸런서 자동 생성, 공개 IP 부여, 비용 발생
클러스터 내에서 외부 DB나 레거시 API를 서비스 이름으로 추상화하고 싶을 때ExternalNameCNAME으로 외부 DNS 이름 매핑, 파드 없음, 외부 서비스 마이그레이션 전환에 유용
Ingress Controller를 통해 HTTP 경로 기반 라우팅이 필요한 경우ClusterIP + Ingress서비스는 ClusterIP, Ingress가 외부 진입점 역할, 단일 로드밸런서로 다수 서비스 라우팅 가능

💡개념

전체 실습 — 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

심화 — 배포할 때마다 몇 초씩 나는 오류

💡개념

심화: Endpoints는 실시간이 아니다 — 종료 중 파드로 가는 트래픽

앞의 TroubleCase는 Endpoints가 비어 있는(selector 불일치) 정적 문제였습니다. 실무에서 더 자주, 그러나 더 잡기 어려운 것은 Endpoints가 채워져 있는데도 롤아웃 순간에만 오류가 나는 동적 문제입니다. 핵심은 "파드를 Service에서 빼는 일은 즉시가 아니다"라는 사실입니다.

파드를 삭제하면(롤링 업데이트·스케일 다운) 두 가지가 서로 독립적으로, 동시에 시작됩니다.

  1. kubelet이 컨테이너에 SIGTERM을 보냅니다 — 파드는 곧바로 종료 절차에 들어갑니다.
  2. 엔드포인트 컨트롤러가 그 파드 IP를 EndpointSlice에서 제거합니다 — 이 변경이 다시 모든 노드의 kube-proxy로 전파되어 iptables/IPVS 규칙에서 빠져야 합니다.

문제는 2번이 최종 일관성(eventually consistent) 이라는 점입니다. 제거가 각 노드에 반영되기까지 보통 수백 ms에서 수 초가 걸립니다. 그 창 동안 파드는 이미 죽어 가는데 일부 노드의 라우팅 규칙엔 아직 남아 있어, 새 연결과 진행 중 요청이 죽어가는 파드로 라우팅됩니다. 그 결과가 배포 때마다 반복되는 502·connection reset의 짧은 스파이크입니다.

그래서 무중단 배포에는 종료 절차가 엔드포인트 제거 전파와 겹치도록 설계해야 합니다.

  • preStop 훅으로 잠깐 대기: 컨테이너가 SIGTERM을 받기 전에 몇 초(예: 5초) 자게 하면, 그동안 파드는 계속 응답하고 엔드포인트 제거가 전 노드에 전파될 시간을 법니다.
  • 앱의 graceful shutdown: SIGTERM을 받으면 새 연결 수신을 멈추고, 진행 중 요청을 드레인한 뒤 종료합니다. terminationGracePeriodSeconds 안에 끝나야 강제 종료(SIGKILL)를 피합니다.
  • readiness probe: 시작 쪽에서도 대칭으로, 준비되기 전 파드가 엔드포인트에 등록돼 트래픽을 먼저 받는 일을 막습니다.

상황: kubectl rollout으로 배포할 때마다 클라이언트 쪽 에러율이 잠깐 튀었다가, 롤아웃이 끝나면 저절로 가라앉습니다. Endpoints를 확인하면 결국 새 파드들로 정상 채워져 있어, selector나 label 문제는 아닙니다.

원인: 종료되는 구 파드가 Endpoints에서 빠지는 전파가 kubelet의 SIGTERM보다 느려서 생기는 레이스입니다. 파드는 이미 종료 중인데 일부 노드의 kube-proxy 규칙엔 남아 있어, 그 짧은 창 동안 트래픽이 죽어가는 파드로 흘러 connection reset이 납니다. 앱이 SIGTERM을 받자마자 즉시 리스너를 닫아 버리면 창이 더 벌어집니다.

진단: 에러 스파이크의 시각이 롤아웃 시각과 정확히 겹치는지 확인합니다(정상 운영 중엔 발생하지 않음). kubectl get pod <pod> -o yaml에서 lifecycle.preStop이 없고 terminationGracePeriodSeconds가 짧은지, 앱이 SIGTERM에서 곧바로 죽는 구조인지 봅니다. 종료 로그에 드레인 없이 즉시 프로세스가 종료된 흔적이 있으면 유력합니다.

해결: 파드에 preStop 훅(exec: sleep 5 또는 앱 서버의 우아한 종료 신호)을 넣어 엔드포인트 제거가 전파되는 동안 계속 응답하게 하고, 앱이 SIGTERM에서 새 연결을 끊고 진행 중 요청을 드레인하도록 구현합니다. terminationGracePeriodSeconds를 드레인 시간보다 넉넉히 잡습니다. 이렇게 하면 Endpoints 제거가 최종 일관성이라는 사실과 무관하게, 파드가 라우팅에서 완전히 빠질 때까지 트래픽을 안전하게 받아넘길 수 있습니다(Deployment를 이용한 안정적인 서비스 배포와 롤백 전략).

💼
실무 맥락
현업 패턴

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

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

내부 마이크로서비스 연결: 항상 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

# 서비스 삭제 (아래 DangerBlock 확인 후 실행)
위험 명령어Service를 삭제하면 해당 Service에 의존하는 모든 Pod의 DNS 조회가 실패합니다. ClusterIP가 해제되고 kube-proxy 규칙도 제거되어, 같은 이름의 Service를 다시 생성해도 IP가 바뀔 수 있습니다. 현재 컨텍스트가 프로덕션 클러스터라면 서비스 중단이 즉시 발생합니다.

Service 삭제 — 클러스터 내 DNS·엔드포인트 즉시 제거

안전한 실행 조건: kubectl config current-context 로 개발 클러스터인지 확인. 실습 네임스페이스(svc-lab)가 맞는지 kubectl get svc -n svc-lab 으로 대상 확인 후 실행하세요.

실행 전 반드시 확인

  • kubectl config current-context 로 개발 클러스터(minikube 등)인지 확인
  • kubectl get svc -n svc-lab 로 삭제 대상 서비스 이름 확인
kubectl delete svc my-web-service -n svc-lab

위 항목을 모두 확인한 후 복사할 수 있습니다

Kubernetes
kubectl delete svc my-web-service -n svc-lab
실습 단계
1

Deployment 및 ClusterIP Service 생성

kubectl create deployment my-web --image=nginx:alpine --replicas=3 -n svc-lab kubectl expose deployment my-web --port=80 --target-port=80 -n svc-lab kubectl get svc my-web -n svc-lab

예상 출력

NAME     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
my-web   ClusterIP   10.96.x.x       none          80/TCP    ...
2

Endpoints 확인 — Selector 매칭 결과

kubectl get endpoints my-web -n svc-lab

예상 출력

NAME     ENDPOINTS
my-web   10.244.x.x:80,10.244.x.x:80,10.244.x.x:80
3

클러스터 내부에서 서비스 DNS 접근 테스트

kubectl run curl-test --image=curlimages/curl -it --rm --restart=Never -n svc-lab -- curl -s -o /dev/null -w '%{http_code}' http://my-web

예상 출력

200
4

NodePort Service 생성 및 확인

kubectl expose deployment my-web --type=NodePort --port=80 --name=my-web-np -n svc-lab kubectl get svc my-web-np -n svc-lab

예상 출력

NAME         TYPE       CLUSTER-IP     PORT(S)        AGE
my-web-np    NodePort   10.96.x.x      80:3xxxx/TCP   ...
5

실습 리소스 정리

kubectl delete svc my-web my-web-np -n svc-lab kubectl delete deployment my-web -n svc-lab echo cleaned

예상 출력

service "my-web" deleted
service "my-web-np" deleted
deployment.apps "my-web" deleted
cleaned

명령어·단축키 빠른 참조

이 모듈에서 Service·Endpoints·Selector 연결을 확인할 때 쓴 kubectl 명령을 모았습니다.

명령어/단축키용도자주 쓰는 예
kubectl get svc타입·CLUSTER-IP·EXTERNAL-IP·포트 확인kubectl get svc -o wide · LoadBalancer는 -w로 IP 대기
kubectl get endpoints실제 연결된 파드 IP 목록kubectl get endpoints my-svc (<none>=selector 불일치)
kubectl describe svcSelector·TargetPort 확인kubectl describe svc X | grep Selector
kubectl get pods --show-labels파드 라벨 vs Selector 대조kubectl get pods --show-labels
kubectl expose deployment서비스 빠르게 생성kubectl expose deploy X --type=NodePort --port=80 --target-port=8080
kubectl get nodes -o wideNodePort 접근용 노드 IP 확인kubectl get nodes -o wide (INTERNAL-IP)
kubectl run … nslookup서비스 DNS 해석 테스트kubectl run t --image=busybox --rm -it -- nslookup my-svc.<ns>
kubectl run … curl클러스터 내부 접속 테스트kubectl run t --image=curlimages/curl --rm -it -- curl http://my-svc
kubectl exec … ss파드가 실제 리스닝하는 포트 확인kubectl exec -it X -- ss -tlnp
minikube service / tunnel로컬 LB/NodePort 접근minikube service X --url · minikube tunnel

관련 모듈로 더 깊이:

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

LoadBalancer 타입 Service는 실제로 클라우드의 로드밸런서를 프로비저닝합니다. 그 로드밸런서가 트래픽 분산·헬스체크·오토스케일과 어떻게 맞물리는지는 오토스케일링 + 로드밸런서(Cloud Engineering 트랙)에서 다룹니다.

지식 확인

퀴즈 — 8문제

Q1

외부 사용자가 API 서버에 접근해야 하는데 type: ClusterIP로 서비스를 만들었다. 접근이 전혀 안 된다. 이 문제를 최소 변경으로 해결하는 방법은?

Q2

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

Q3

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

Q4

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

Q5

Service를 만들었는데 접속이 안 된다. kubectl get endpoints <svc> 결과가 비어 있다. 1차 원인은?

Q6

사내 마이크로서비스끼리만 통신하고 외부 노출이 전혀 필요 없는 서비스에 적절한 타입은?

Q7

[심화] 파드를 삭제하면 kubelet의 SIGTERM 전송과 Endpoints에서의 파드 제거가 동시에 시작된다. 이 사실이 롤아웃 중 순간 오류를 만드는 이유는?

Q8

[심화] 롤링 업데이트 때마다 몇 초간 connection reset이 몰리고 배포가 끝나면 사라진다. 가장 효과적인 대응은?

0 / 8 답변

🧪 실습으로 확인하기

Kubernetes Ingress 라우팅 — 404/503 디버깅

중급

port-forward로 파드는 되는데 도메인 접속만 404/503이다. Ingress 라우팅은 Ingress→Service→Endpoints(Pod)의 사슬이다. 어느 고리가 끊겼는지(호스트/경로 규칙, 서비스 셀렉터, 엔드포인트 비어있음, Ingress 컨트롤러)를 단계별로 좁혀 특정하고 복구한다.

45📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요