근묵자흑
Kubernetes Pattern: Service Discovery 본문
"동적으로 변화하는 컨테이너 환경에서 서비스는 어떻게 서로를 찾을까?"
마이크로서비스 아키텍처에서 Pod들은 끊임없이 생성되고 삭제됩니다. IP 주소가 계속 바뀌는 이런 환경에서 안정적인 서비스 간 통신을 보장하는 것이 바로 Service Discovery 패턴입니다.
이 글에서는 Kubernetes의 4가지 Service Discovery 방식을 Minikube 로컬 환경에서 실습한 사항과 그에 따른 여러 컴포넌트를 확인해보겠습니다.
1. Service Discovery가 필요한 이유
문제 상황: 변하는 IP 주소
# Pod가 재시작되면 IP가 바뀜
Pod A (10.244.1.5) → 재시작 → Pod A' (10.244.2.7)
# 스케일링하면 여러 개의 Pod가 생김
Pod B (10.244.1.6) → 스케일링 → Pod B1, B2, B3... (각각 다른 IP)
애플리케이션이 다른 서비스를 호출할 때 IP를 하드코딩하면 어떻게 될까요? Pod가 재시작되거나 스케일링될 때마다 코드를 수정해야 하는 악몽이 펼쳐집니다.
해결책: Service 리소스
# Service는 안정적인 엔드포인트 제공
Service (10.96.10.10) → 자동으로 건강한 Pod들로 라우팅
Kubernetes Service는 변하지 않는 ClusterIP와 DNS 이름을 제공하여, 백엔드 Pod가 어떻게 변하든 안정적인 접근을 보장합니다.
2. 실습 환경
사용 기술 스택
- Minikube: v1.36.0
- 애플리케이션: random-generator (랜덤 숫자 JSON 반환)
테스트 애플리케이션 배포
4개의 레플리카로 구성된 random-generator 애플리케이션을 배포합니다:
# deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: random-generator
spec:
replicas: 4
selector:
matchLabels:
app: random-generator
template:
metadata:
labels:
app: random-generator
spec:
containers:
- name: random-generator
image: k8spatterns/random-generator:1.0
ports:
- containerPort: 8080
kubectl apply -f deployment.yml
kubectl rollout status deployment/random-generator
배포 결과:
NAME READY STATUS RESTARTS AGE
random-generator-77d64667b4-b2t6k 1/1 Running 0 8m
random-generator-77d64667b4-m9zrt 1/1 Running 0 8m
random-generator-77d64667b4-qrsn4 1/1 Running 0 8m
random-generator-77d64667b4-sfdq9 1/1 Running 0 8m
4개의 Pod가 각각 다른 IP 주소를 가지고 실행됩니다. 이제 이들을 어떻게 찾을까요?
3. ClusterIP: 내부 Service Discovery
개념
ClusterIP는 Kubernetes의 기본 Service 타입으로, 클러스터 내부에서만 접근 가능한 가상 IP를 제공합니다.
Service 생성
# service.yml
apiVersion: v1
kind: Service
metadata:
name: random-generator
spec:
type: ClusterIP
selector:
app: random-generator
ports:
- port: 80
targetPort: 8080
kubectl apply -f service.yml
kubectl get svc random-generator
결과:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
random-generator ClusterIP 10.109.121.11 <none> 80/TCP 7m
Service가 10.109.121.11이라는 안정적인 ClusterIP를 받았습니다.
DNS 해석 테스트
Kubernetes는 자동으로 Service에 대한 DNS 레코드를 생성합니다:
kubectl run dns-test --image=busybox:1.28 --rm -i --restart=Never -- \
nslookup random-generator.default.svc.cluster.local
출력:
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: random-generator.default.svc.cluster.local
Address 1: 10.109.121.11 random-generator.default.svc.cluster.local
핵심 발견:
- DNS 서버는
kube-dns(CoreDNS) - FQDN이 ClusterIP로 정확히 해석됨
- 응답 시간 < 20ms (매우 빠름)
DNS 이름 형식
Kubernetes에서는 3가지 형식으로 Service를 호출할 수 있습니다:
# 1. Short name (같은 namespace에서)
curl http://random-generator/
# 2. Service.Namespace
curl http://random-generator.default/
# 3. FQDN (Fully Qualified Domain Name)
curl http://random-generator.default.svc.cluster.local/
HTTP 요청 테스트
실제로 Service를 통해 애플리케이션에 접근해봅시다:
kubectl run http-test --image=curlimages/curl --rm -i --restart=Never -- \
curl -s http://random-generator/
응답:
{"random":1445296461,"id":"16ed85f1-aabe-4605-888f-48970b77e69d","version":"1.0"}
성공! Service 이름만으로 간단히 접근할 수 있습니다.
로드밸런싱 검증
Service는 어떻게 4개의 Pod 중 하나를 선택할까요? 5번 연속 요청해봅시다:
kubectl run load-test --image=curlimages/curl --rm -i --restart=Never -- \
sh -c 'for i in 1 2 3 4 5; do curl -s http://random-generator/ | grep -o "\"id\":\"[^\"]*\""; done'
결과:
Request 1: "id":"4877aa43-6372-4376-b428-0ab7a2743bd9" ← Pod 1
Request 2: "id":"16ed85f1-aabe-4605-888f-48970b77e69d" ← Pod 2
Request 3: "id":"16ed85f1-aabe-4605-888f-48970b77e69d" ← Pod 2
Request 4: "id":"16ed85f1-aabe-4605-888f-48970b77e69d" ← Pod 2
Request 5: "id":"8c219d56-4f02-4ec4-b5bc-eeedd284979a" ← Pod 3
각 요청의 id 값이 다른 것은 서로 다른 Pod에서 응답했다는 뜻입니다. kube-proxy가 iptables 규칙을 사용해 트래픽을 분산하고 있습니다.
Endpoints 확인
Service는 어떤 Pod로 트래픽을 보낼지 어떻게 알까요? Endpoints 리소스가 그 비밀입니다:
kubectl get endpoints random-generator -o wide
출력:
NAME ENDPOINTS
random-generator 10.244.3.55:8080,10.244.3.56:8080,10.244.3.57:8080,10.244.3.58:8080
Service의 selector와 일치하는 모든 Pod IP가 자동으로 등록됩니다.
EndpointSlice (Kubernetes 1.21+)
Kubernetes 1.21부터는 확장성이 더 좋은 EndpointSlice API를 사용합니다:
kubectl get endpointslices -l kubernetes.io/service-name=random-generator -o yaml
핵심 정보:
endpoints:
- addresses: [10.244.3.55]
conditions:
ready: true
serving: true
terminating: false
targetRef:
kind: Pod
name: random-generator-77d64667b4-sfdq9
각 Pod의 상태를 더 상세히 추적할 수 있습니다:
ready: 트래픽 받을 준비 완료serving: 현재 트래픽 수신 중terminating: 종료 중 (트래픽 차단)
동작 원리 요약
[Client Pod]
↓
DNS Query: random-generator
↓
[CoreDNS] → 10.109.121.11 (ClusterIP)
↓
[kube-proxy iptables rules]
↓ (로드밸런싱)
├─→ Pod 1: 10.244.3.55:8080
├─→ Pod 2: 10.244.3.56:8080
├─→ Pod 3: 10.244.3.57:8080
└─→ Pod 4: 10.244.3.58:8080
4. NodePort: 노드 레벨 외부 접근
개념
NodePort는 클러스터의 모든 노드에 특정 포트(30000-32767)를 열어 외부에서 접근할 수 있게 합니다.
Service 생성
# service-nodeport.yml
apiVersion: v1
kind: Service
metadata:
name: random-generator-nodeport
spec:
type: NodePort
selector:
app: random-generator
ports:
- port: 80
targetPort: 8080
nodePort: 30080
kubectl apply -f service-nodeport.yml
kubectl get svc random-generator-nodeport
결과:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
random-generator-nodeport NodePort 10.97.245.38 <none> 80:30080/TCP 6m
80:30080은 "Service Port 80이 NodePort 30080으로 매핑됨"을 의미합니다.
Minikube에서 접근하기
Minikube의 Docker driver는 직접 NodePort 접근을 지원하지 않습니다. 대신 minikube service 명령을 사용합니다:
minikube service random-generator-nodeport --url
출력:
http://127.0.0.1:64678
Minikube가 자동으로 터널을 생성하여 로컬호스트 포트로 포워딩합니다.
테스트
curl http://127.0.0.1:64678/
응답:
{"random":200244791,"id":"16ed85f1-aabe-4605-888f-48970b77e69d","version":"1.0"}
5. LoadBalancer: 외부 로드밸런서
개념
LoadBalancer 타입은 클라우드 제공자의 로드밸런서를 자동으로 프로비저닝합니다. AWS ELB, GCP Load Balancer 등이 자동으로 생성됩니다.
Service 생성
# service-loadbalancer.yml
apiVersion: v1
kind: Service
metadata:
name: random-generator-lb
spec:
type: LoadBalancer
selector:
app: random-generator
ports:
- port: 80
targetPort: 8080
kubectl apply -f service-loadbalancer.yml
kubectl get svc random-generator-lb
초기 상태:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
random-generator-lb LoadBalancer 10.103.90.56 <pending> 80:30146/TCP 4m
Minikube는 클라우드 환경이 아니므로 External IP가 <pending> 상태로 남습니다.
Minikube Tunnel
Minikube에서 LoadBalancer를 테스트하려면 터널을 사용합니다:
minikube tunnel
출력:
* Starting tunnel for service random-generator-lb
* NOTE: Please do not close this terminal
이제 Service를 다시 확인하면:
kubectl get svc random-generator-lb
결과:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
random-generator-lb LoadBalancer 10.103.90.56 127.0.0.1 80:30146/TCP 4m
External IP가 127.0.0.1로 할당되었습니다!
클러스터 내부 테스트
kubectl run lb-test --image=curlimages/curl --rm -i --restart=Never -- \
curl -s http://random-generator-lb/
응답:
{"random":-349459181,"id":"8c219d56-4f02-4ec4-b5bc-eeedd284979a","version":"1.0"}
LoadBalancer의 계층 구조
[External User]
↓
[Cloud Load Balancer] ← External IP (퍼블릭)
↓
[NodePort: 30146] ← 자동 생성
↓
[ClusterIP: 10.103.90.56]
↓
[Pods]
LoadBalancer는 사실 ClusterIP + NodePort + External Endpoint의 조합입니다.
실무 사용 시나리오
AWS EKS 예시:
apiVersion: v1
kind: Service
metadata:
name: production-api
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
spec:
type: LoadBalancer
loadBalancerSourceRanges:
- 203.0.113.0/24 # 특정 IP만 허용
ports:
- port: 443
targetPort: 8443
비용 고려사항:
- LoadBalancer 하나당 클라우드 비용 발생
- 여러 서비스를 노출할 때는 Ingress 권장
6. Ingress: HTTP 라우팅
개념
Ingress는 L7 (HTTP/HTTPS) 레벨에서 트래픽을 라우팅합니다. 하나의 External IP로 여러 서비스를 호스트 이름이나 경로로 구분할 수 있습니다.
Ingress Controller 설치
Minikube는 Nginx Ingress Controller를 애드온으로 제공합니다:
minikube addons enable ingress
출력:
* Using image registry.k8s.io/ingress-nginx/controller:v1.12.2
* Verifying ingress addon...
* The 'ingress' addon is enabled
Controller가 준비될 때까지 대기:
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=90s
Ingress 리소스 생성
# ingress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: random-generator-ingress
spec:
ingressClassName: nginx
rules:
- host: random.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: random-generator
port:
number: 80
kubectl apply -f ingress.yml
kubectl get ingress
결과:
NAME CLASS HOSTS ADDRESS PORTS AGE
random-generator-ingress nginx random.local 192.168.49.2 80 2m
클러스터 내부 테스트
Ingress는 Host 헤더를 기반으로 라우팅하므로, 요청 시 헤더를 포함해야 합니다:
kubectl run ingress-test --image=curlimages/curl --rm -i --restart=Never -- \
curl -s -H "Host: random.local" http://ingress-nginx-controller.ingress-nginx/
응답:
{"random":-800948098,"id":"4877aa43-6372-4376-b428-0ab7a2743bd9","version":"1.0"}
성공! Ingress가 random.local 호스트를 인식하고 올바른 Service로 라우팅했습니다.
호스트에서 접근하기 (선택사항)
로컬 머신에서 접근하려면 /etc/hosts를 설정합니다:
echo "$(minikube ip) random.local" | sudo tee -a /etc/hosts
Ingress Controller의 NodePort 확인:
kubectl get svc -n ingress-nginx
출력:
NAME TYPE CLUSTER-IP PORT(S)
ingress-nginx-controller NodePort 10.110.41.35 80:32279/TCP
이제 브라우저나 curl로 접근:
curl -H "Host: random.local" http://$(minikube ip):32279/
라우팅 예제
경로 기반 라우팅
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: multi-service-ingress
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /v1
pathType: Prefix
backend:
service:
name: api-v1
port:
number: 8080
- path: /v2
pathType: Prefix
backend:
service:
name: api-v2
port:
number: 8080
하나의 도메인으로 버전별 API를 분리할 수 있습니다.
TLS/SSL 설정
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: secure-ingress
spec:
ingressClassName: nginx
tls:
- hosts:
- api.example.com
secretName: api-tls-secret
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
Ingress vs LoadBalancer
| 구분 | LoadBalancer | Ingress |
|---|---|---|
| OSI Layer | L4 (Transport) | L7 (Application) |
| 프로토콜 | TCP/UDP | HTTP/HTTPS |
| 라우팅 | IP/Port만 | Host, Path, Header 등 |
| SSL 종료 | 불가 | 가능 |
| 비용 | 서비스당 LB | 하나의 LB로 여러 서비스 |
| 사용 사례 | Non-HTTP 서비스 | 웹 애플리케이션 |
권장:
- HTTP/HTTPS 트래픽 → Ingress
- TCP/UDP (DB, gRPC) → LoadBalancer
- 내부 통신 → ClusterIP
7. DNS 성능 분석
DNS가 Service Discovery의 핵심
Kubernetes에서 Service Discovery의 90%는 DNS를 통해 이루어집니다. 성능이 중요한 이유입니다.
벤치마크 테스트
50회 연속 DNS 조회 성능을 측정합니다:
kubectl run dns-perf --image=busybox:1.28 --rm -i --restart=Never -- \
sh -c 'START=$(date +%s); \
for i in $(seq 1 50); do \
nslookup random-generator.default.svc.cluster.local > /dev/null 2>&1; \
done; \
END=$(date +%s); \
echo "Total: $((END - START))s"'
결과:
Total: 0s
Average: < 20ms per query
50회 조회가 1초도 안 걸립니다! CoreDNS의 캐싱 덕분입니다.
CoreDNS 아키텍처
kubectl get pods -n kube-system -l k8s-app=kube-dns
출력:
NAME READY STATUS RESTARTS AGE
coredns-5dd5756b68-xxxxx 1/1 Running 0 15m
CoreDNS는 클러스터의 DNS 서버로, 모든 Service와 Pod에 대한 DNS 레코드를 관리합니다.
DNS 캐싱 확인
Pod의 /etc/resolv.conf:
kubectl run resolv-test --image=busybox:1.28 --rm -i --restart=Never -- \
cat /etc/resolv.conf
출력:
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
핵심 설정:
nameserver: CoreDNS 주소search: 도메인 검색 경로 (short name 사용 가능하게 함)ndots:5: 점이 5개 미만이면 search 도메인 추가
DNS Policy 종류
Kubernetes는 4가지 DNS Policy를 제공합니다:
# 1. ClusterFirst (기본값)
dnsPolicy: ClusterFirst
# 2. Default (노드의 DNS 사용)
dnsPolicy: Default
# 3. None (수동 설정)
dnsPolicy: None
dnsConfig:
nameservers:
- 8.8.8.8
# 4. ClusterFirstWithHostNet
dnsPolicy: ClusterFirstWithHostNet
성능 최적화
1. NodeLocal DNSCache 사용
kubectl apply -f https://k8s.io/examples/admin/dns/nodelocaldns.yaml
각 노드에 DNS 캐시를 배치하여 CoreDNS 부하를 줄이고 레이턴시를 감소시킵니다.
2. DNS 캐싱 튜닝
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
cluster.local:53 {
cache 30 # 30초 캐싱
kubernetes cluster.local {
ttl 10
}
}
3. FQDN 사용
// 나쁜 예: 여러 번 DNS 조회 시도
http.Get("http://api-service/")
// 좋은 예: 한 번에 정확히 조회
http.Get("http://api-service.default.svc.cluster.local/")
ndots:5 설정으로 인해 short name은 최대 5번의 DNS 조회가 발생할 수 있습니다.
8. 실무적 관점에선
Service Discovery 선택 가이드
┌────────────────────┐
│ 접근 범위가 어디인가? │
└────────────────────┘
│
┌──────┴──────┐
│ │
클러스터 내부 클러스터 외부
│ │
↓ ↓
ClusterIP NodePort/LB/Ingress
│
┌─────┴─────┐
│ │
개발/테스트 프로덕션
│ │
↓ ↓
NodePort LB/Ingress
│
┌─────┴─────┐
│ │
HTTP/HTTPS 기타 프로토콜
│ │
↓ ↓
Ingress LoadBalancer
4가지 타입 한눈에 비교
| Service 타입 | ClusterIP | NodePort | LoadBalancer | Ingress |
|---|---|---|---|---|
| External IP | X | X | O | O |
| 외부 접근 | X | O | O | O |
| DNS 이름 | O | O | O | O (Host 기반) |
| 로드밸런싱 | L4 | L4 | L4 | L7 |
| SSL/TLS | X | X | X | O |
| 비용 | 무료 | 무료 | 비용 | 비용 (하나로 다수 서비스) |
| 사용 사례 | 내부 통신 | 개발/테스트 | 프로덕션 외부 노출 | 웹 앱 라우팅 |
Minikube(테스트 환경) vs 클라우드 서비스 환경 차이점
Minikube 제약사항
| 기능 | Minikube (Docker driver) | AWS/GCP |
|---|---|---|
| NodePort 접근 | minikube service 필요 |
<NodeIP>:30080 직접 접근 |
| LoadBalancer | minikube tunnel 필요 |
자동으로 ELB/GLB 생성 |
| Ingress | NodePort 우회 | External IP 자동 할당 |
| 멀티 노드 | 단일 노드만 | 실제 분산 환경 |
1. ClusterIP + Ingress 조합 (권장)
장점:
- 하나의 LoadBalancer로 여러 서비스 노출
- 자동 SSL/TLS 인증서 관리 (cert-manager)
- Host/Path 기반 라우팅
- 비용 효율적
2. Session Affinity 설정
apiVersion: v1
kind: Service
metadata:
name: stateful-service
spec:
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800 # 3시간
WebSocket이나 로그인 세션처럼 상태를 유지해야 하는 경우 사용합니다.
3. Headless Service (StatefulSet)
apiVersion: v1
kind: Service
metadata:
name: database
spec:
clusterIP: None # Headless
selector:
app: postgres
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: database
replicas: 3
각 Pod에 안정적인 DNS 이름 부여:
postgres-0.database.default.svc.cluster.localpostgres-1.database.default.svc.cluster.localpostgres-2.database.default.svc.cluster.local
데이터베이스 클러스터처럼 개별 인스턴스에 접근해야 할 때 사용합니다.
보안 고려사항
NetworkPolicy로 접근 제한
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-access-policy
spec:
podSelector:
matchLabels:
app: backend-api
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
frontend Pod만 backend-api에 접근 가능하도록 제한합니다.
LoadBalancer 소스 IP 제한
apiVersion: v1
kind: Service
metadata:
name: admin-api
spec:
type: LoadBalancer
loadBalancerSourceRanges:
- 203.0.113.0/24 # 회사 사무실 IP만
모니터링 및 디버깅
Service 연결 문제 디버깅
# 1. Service와 Endpoints 확인
kubectl get svc,endpoints
# 2. DNS 해석 테스트
kubectl run -it --rm debug --image=nicolaka/netshoot --restart=Never -- bash
nslookup my-service
dig my-service.default.svc.cluster.local
# 3. 직접 Pod IP로 접근 (Service 우회)
curl http://<pod-ip>:8080
# 4. kube-proxy 로그 확인
kubectl logs -n kube-system -l k8s-app=kube-proxy
# 5. CoreDNS 로그
kubectl logs -n kube-system -l k8s-app=kube-dns
일반적인 문제와 해결책
| 증상 | 원인 | 해결책 |
|---|---|---|
| DNS 해석 실패 | CoreDNS 장애 | kubectl get pods -n kube-system |
| Endpoints 없음 | Label selector 불일치 | kubectl describe svc |
| 503 Service Unavailable | Pod가 모두 NotReady | kubectl get pods |
| Connection timeout | NetworkPolicy 차단 | kubectl get networkpolicies |
요약
Service Discovery 동작 원리
Application Code
↓
curl http://my-service/
↓
DNS Query to CoreDNS (10.96.0.10)
↓
ClusterIP 반환 (e.g., 10.109.121.11)
↓
kube-proxy iptables/IPVS rules
↓
로드밸런싱 (라운드로빈)
↓
Pod Endpoint (10.244.x.x:8080)
4가지 방식 언제 사용할까?
- ClusterIP
- 마이크로서비스 간 내부 통신
- 데이터베이스, 캐시, 메시지 큐
- 외부 노출 불필요한 모든 서비스
- NodePort
- 로컬 개발 환경
- 빠른 프로토타입
- CI/CD 테스트
- 프로덕션 비추천
- LoadBalancer
- TCP/UDP 서비스 외부 노출
- gRPC 서버
- 레거시 애플리케이션
- Ingress 사용 불가한 경우
- Ingress
- HTTP/HTTPS 웹 애플리케이션
- RESTful API
- 여러 서비스를 하나의 도메인으로
- SSL/TLS 자동 관리
참고 자료:
'k8s > kubernetes-pattern' 카테고리의 다른 글
| Kubernetes Pattern: Self Awareness (0) | 2025.11.08 |
|---|---|
| Service Discovery 심화: Knative (2) | 2025.11.01 |
| Kubernetes Pattern: Stateless Service (8) | 2025.10.18 |
| Kubernetes Pattern: Stateful Service (0) | 2025.10.11 |
| Kubernetes Pattern: Singleton Service (2) | 2025.09.27 |