파드는 정상인데 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의 핵심 역할
클라이언트
│
▼
┌─────────────────────────────┐
│ 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이 일치하는 파드가 새로 생기면 자동으로 연결에 포함되고, 파드가 죽으면 자동으로 제외됩니다.
# 파드 (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 목록
- NAME—조회 대상 리소스 이름이 예상한 대상과 일치하는지 확인합니다.
- STATUS/READY—Running, Ready, Available처럼 정상 상태를 나타내는 필드가 있는지 봅니다.
- RESTARTS/EVENTS—재시작 횟수나 Warning 이벤트가 증가하지 않는지 확인합니다.
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
트래픽 흐름
외부 클라이언트
│
▼
노드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를 포함하며, 클라우드 컨트롤러가 실제 로드밸런서와 연동합니다.
# 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
실무에서 서비스 타입 선택하는 기준
현업에서 쿠버네티스 클러스터를 운영할 때 서비스 타입은 트래픽의 방향과 환경에 따라 결정합니다.
내부 마이크로서비스 연결: 항상 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
# 서비스 삭제
kubectl delete svc my-web-service -n svc-lab
다음 챕터에서는 Ingress Controller로 경로 기반 라우팅과 TLS를 처리하는 방법을 학습합니다.