파드는 정상인데 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-infokubectl get nodeskubectl create namespace svc-labkubectl config current-contextService란 무엇인가 — 파드 앞에 놓이는 안정적인 진입점
프론트엔드 팀에서 "API 서버 IP가 또 바뀌었냐"고 문의가 왔습니다. Deployment를 재배포할 때마다 파드 IP가 바뀌는데, 클라이언트가 파드 IP를 직접 사용하고 있었던 겁니다. 파드를 스케일 아웃하면 어떤 파드로 보내야 하는지도 클라이언트가 직접 결정해야 하는 문제도 생깁니다. Service는 파드 IP 변화를 추상화하는 안정적인 진입점입니다. 파드가 죽어도, 스케일이 바뀌어도 Service의 IP와 DNS 이름은 변하지 않습니다.
파드는 언제든지 재시작될 수 있고, Deployment가 스케일 아웃하면 파드 수가 늘어납니다. 각 파드가 고유한 IP를 가지지만 이 IP는 파드가 죽으면 사라집니다. 클라이언트가 파드 IP를 직접 사용한다면, 파드가 재시작될 때마다 설정을 변경해야 합니다. Service는 이 문제를 해결합니다.
확대
Service의 핵심 역할
확대
Selector와 label 매칭
Service는 spec.selector에 정의된 label과 일치하는 파드를 자동으로 탐지합니다. label이 일치하는 파드가 새로 생기면 자동으로 연결에 포함되고, 파드가 죽으면 자동으로 제외됩니다.
# 파드 (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 목록이 채워집니다.
# 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 → 파드 전달이 순서대로 일어납니다. 이 흐름을 알면 "서비스는 있는데 왜 연결이 안 되지"를 단계로 좁혀 진단할 수 있습니다.
[클라이언트 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 에서 수신·응답
각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:
| 단계 | 하는 일 | 여기서 막히면 |
|---|---|---|
| ① DNS | CoreDNS가 my-service.<namespace>.svc.cluster.local을 ClusterIP로 해석 | CoreDNS 장애·이름 오타 → Could not resolve host, nslookup 실패 |
| ② 가상 IP | ClusterIP(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 생성
# 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
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
# 클러스터 내부 파드에서 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 생성
# 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
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
트래픽 흐름
확대
LoadBalancer와 ExternalName — 클라우드 연동과 외부 서비스 추상화
EKS나 GKE 같은 관리형 클러스터에서 서비스를 인터넷에 공개해야 할 때, 각 서비스마다 LoadBalancer를 만들면 클라우드 비용이 서비스 수만큼 증가합니다. 또한 외부에서 운영하는 레거시 데이터베이스를 이전하는 중이라면, 코드 변경 없이 목적지를 바꿀 수 있는 추상화가 필요합니다. LoadBalancer는 클라우드 제공자의 로드밸런서를 자동으로 프로비저닝해 공인 IP를 할당하고, ExternalName은 외부 DNS 이름을 클러스터 내부 서비스처럼 참조할 수 있게 합니다.
LoadBalancer — 클라우드 로드밸런서 자동 프로비저닝
LoadBalancer 타입은 클라우드 제공자(AWS ELB, GCP Cloud Load Balancing, Azure Load Balancer)의 로드밸런서를 자동으로 생성하고 공인 IP를 할당합니다. 내부적으로 NodePort를 포함하며, 클라우드 컨트롤러가 실제 로드밸런서와 연동합니다.
# 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
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를 클러스터 내부 서비스처럼 참조할 때 유용합니다.
# externalname-service.yaml
apiVersion: v1
kind: Service
metadata:
name: external-db
namespace: svc-lab
spec:
type: ExternalName
externalName: my-database.example.com # 외부 DNS 이름
# selector 없음
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를 함께 배포합니다.
# 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
# 배포
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'
문제 상황
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 불일치 판별
kubectl get endpoints my-web-service -n svc-lab
# NAME ENDPOINTS AGE
# my-web-service <none> 2m
# ↑ 빈 값 = Selector와 일치하는 파드가 없음
Endpoints가 <none>이면 Selector 불일치가 원인입니다.
진단 2: 파드 label 확인
# 파드에 실제로 붙어 있는 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 확인
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에 맞춤)
# service 수정
spec:
selector:
app: my-webapp # 파드 label과 일치하도록 수정
방법 2: Deployment label 수정 (Service Selector에 맞춤)
# deployment template label 수정
template:
metadata:
labels:
app: my-web # Service Selector와 일치하도록 수정
# 수정 적용 후 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 ← 정상!
체크리스트
# 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에서 빼는 일은 즉시가 아니다"라는 사실입니다.
파드를 삭제하면(롤링 업데이트·스케일 다운) 두 가지가 서로 독립적으로, 동시에 시작됩니다.
- kubelet이 컨테이너에 SIGTERM을 보냅니다 — 파드는 곧바로 종료 절차에 들어갑니다.
- 엔드포인트 컨트롤러가 그 파드 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
# 예: 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 활용
# 데이터베이스를 클러스터 외부에서 운영 중인 경우
apiVersion: v1
kind: Service
metadata:
name: postgres-external
spec:
type: ExternalName
externalName: prod-db.internal.company.com
# 코드는 postgres-external:5432로 연결
# 실제 DB를 클러스터로 이전할 때 ExternalName만 제거하면 됨
Service 이름을 코드에서 환경변수로 참조
# 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)
정리
# 서비스 타입별 생성 빠른 참고
# 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 삭제 — 클러스터 내 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위 항목을 모두 확인한 후 복사할 수 있습니다
kubectl delete svc my-web-service -n svc-lab
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 ...
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
클러스터 내부에서 서비스 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
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 ...
실습 리소스 정리
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 svc | Selector·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 wide | NodePort 접근용 노드 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 경로 기반 포워딩과 SSL/TLS 설정 — Service 앞단에서 경로·호스트 기반 L7 라우팅과 TLS를 처리하는 다음 계층
- Deployment를 이용한 안정적인 서비스 배포와 롤백 전략 — Service가 트래픽을 보내는 대상인 파드를 선언적으로 띄우는 법
- NetworkPolicy를 활용한 특정 파드 간 통신 차단 및 제한 — Service로 열린 파드 간 통신을 라벨 기반으로 제한하는 보안 계층
- Docker 네트워크 드라이브 구조와 DNS 기반 컨테이너 연결 — 단일 호스트에서 컨테이너를 DNS 이름으로 연결하는 Docker 네트워크, Service 디스커버리의 원형 (Docker 트랙)
다음 챕터에서는 Ingress Controller로 경로 기반 라우팅과 TLS를 처리하는 방법을 학습합니다.
LoadBalancer타입 Service는 실제로 클라우드의 로드밸런서를 프로비저닝합니다. 그 로드밸런서가 트래픽 분산·헬스체크·오토스케일과 어떻게 맞물리는지는 오토스케일링 + 로드밸런서(Cloud Engineering 트랙)에서 다룹니다.