Notice
Recent Posts
Recent Comments
Link
«   2025/12   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Archives
Today
Total
관리 메뉴

근묵자흑

Kubernetes Pattern: Service Discovery 본문

k8s/kubernetes-pattern

Kubernetes Pattern: Service Discovery

Luuuuu 2025. 10. 25. 19:39

"동적으로 변화하는 컨테이너 환경에서 서비스는 어떻게 서로를 찾을까?"

 

마이크로서비스 아키텍처에서 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.local
  • postgres-1.database.default.svc.cluster.local
  • postgres-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가지 방식 언제 사용할까?

  1. ClusterIP
    • 마이크로서비스 간 내부 통신
    • 데이터베이스, 캐시, 메시지 큐
    • 외부 노출 불필요한 모든 서비스
  2. NodePort
    • 로컬 개발 환경
    • 빠른 프로토타입
    • CI/CD 테스트
    • 프로덕션 비추천
  3. LoadBalancer
    • TCP/UDP 서비스 외부 노출
    • gRPC 서버
    • 레거시 애플리케이션
    • Ingress 사용 불가한 경우
  4. Ingress
    • HTTP/HTTPS 웹 애플리케이션
    • RESTful API
    • 여러 서비스를 하나의 도메인으로
    • SSL/TLS 자동 관리

참고 자료: