0. Contents


1. Overview

Service Discovery 패턴의 핵심Pod IP는 계속 바뀐다 → 안정적인 엔드포인트가 필요하다내부 Pod끼리ClusterIP / DNS외부 → 내부NodePort / LB / Ingress내부 → 외부ExternalName
통신 방향에 따라 쓰는 메커니즘이 갈린다

애플리케이션의 통신 방향

  • Kubernetes에 배포된 애플리케이션은 거의 단독으로 존재하지 않는다
  • 다양한 방향의 네트워크 통신이 필요하다
    • 클러스터 내부의 다른 서비스와 통신
    • 외부 사용자의 요청 수신
    • 외부 DB 연결 등

문제: 동적으로 바뀌는 Pod IP

  • Pod가 재시작되거나 다른 노드로 옮겨지면 IP가 바뀐다
  • 스케일 업/다운 시 Pod 개수도 달라진다

Service Discovery 패턴의 해법

  • Service Discovery 패턴은 이 문제를 해결하기 위해 안정적인 엔드포인트를 제공한다
    • 소비자(Consumer)가 항상 같은 주소로 서비스 제공자(Producer)를 찾아갈 수 있게 한다

2. Problem

2.1 왜 Pod IP를 직접 쓰면 안 되나

Pod IP의 동적 할당

  • Pod가 생성될 때 IP가 동적으로 할당된다
시점결제 Pod
배포 직후10.108.0.5
Pod 재시작10.108.1.23 (바뀜!)
스케일 업3개로 늘어남 (어디로 보내지?)

Pod IP 직접 접속의 한계

  • 다른 서비스가 Pod IP를 직접 알고 접속한다면, Pod가 재시작될 때마다 주소를 갱신해야 한다
  • 이를 개발자가 직접 추적·등록·발견하는 것은 사실상 불가능에 가깝다

2.2 쿠버네티스 이전 시대: Client-Side Discovery

Client-Side Discovery 방식

  • Kubernetes 이전에는 Client-Side Discovery 방식이 일반적이었다
  • 서비스 소비자(Consumer)가 레지스트리(Registry)에서 서비스 목록을 직접 조회했다
  • 어떤 인스턴스로 요청을 보낼지도 스스로 선택했다
서비스 레지스트리(ZooKeeper / Consul)서비스 인스턴스인스턴스 A인스턴스 B인스턴스 C소비자1. 목록 받아서2. 직접 하나 선택3. 요청 전송↑ 등록↓ 조회요청
소비자가 목록을 받아 직접 고른다

레지스트리: “서비스 주소록”

  • 레지스트리는 쉽게 말해 “서비스 주소록” 이다
  • 서비스가 뜰 때 자신의 IP를 등록하고, 죽을 때 삭제한다
  • 소비자는 레지스트리에서 목록을 받아 직접 인스턴스를 골라 통신한다

대표적인 구현체

도구설명
ZooKeeper분산 코디네이션 서비스, 서비스 등록/조회
Consul서비스 디스커버리 + 헬스체크
RibbonNetflix OSS, 클라이언트 사이드 로드밸런서

Client-Side 방식의 복잡도

  • 소비자마다 디스커버리 로직을 직접 구현해야 한다
  • 언어/프레임워크마다 에이전트가 달라진다
  • 레지스트리 운영도 별도로 필요하다
  • → 복잡도가 높다

2.3 쿠버네티스 이후 시대: Server-Side Discovery

플랫폼 레벨로 올라간 책임

  • Kubernetes는 이 모든 책임을 플랫폼 레벨로 끌어올렸다
  • 개발자가 직접 구현하던 일을 전부 Kubernetes가 자동으로 처리한다
    • 배치, 헬스체크, 자가치유, 서비스 디스커버리, 로드밸런싱
예전 (Client-Side)소비자가 레지스트리 조회직접 선택요청지금 (Server-Side)소비자Service(고정 엔드포인트)만 호출Kubernetes가 알아서Pod 선택 후 전달
고르는 주체가 소비자에서 Kubernetes로 옮겨 갔다

개발자의 몫: 비즈니스 코드

  • 개발자는 더 이상 디스커버리 코드를 짤 필요가 없다
  • 비즈니스 코드만 작성하면 Kubernetes가 나머지를 처리한다

3. Internal Service Discovery

3.1 ClusterIP — Service 리소스

Service 리소스

  • Kubernetes에서 내부 서비스 디스커버리의 핵심은 Service 리소스다
  • 동일한 기능을 제공하는 Pod 집합에 대해 고정적이고 안정적인 진입점(ClusterIP)을 제공한다
# Example 13-1. A simple Service
apiVersion: v1
kind: Service
metadata:
  name: random-generator        # ① DNS 이름으로 사용됨 — random-generator.default.svc.cluster.local
spec:
  selector:
    app: random-generator       # ② 이 라벨 가진 Pod들을 묶음 — Pod 생성 순서와 무관하게 매칭
  ports:
    - port: 80                  # ③ 클라이언트가 접근하는 포트 (ClusterIP:80)
      targetPort: 8080          # ④ 실제 Pod가 듣는 포트 — 컨테이너 내부 포트로 전달
      protocol: TCP             # ⑤ 기본값 TCP, UDP/SCTP도 지정 가능

ClusterIP 자동 할당

  • Service가 생성되면 Kubernetes는 자동으로 ClusterIP(가상 IP) 를 할당한다
  • 이 IP는 Service가 존재하는 한 절대 바뀌지 않는다
클라이언트 PodClusterIP: 10.109.0.5(고정, 가상IP)Kubernetes가 실제 Pod IP로 변환Pod APod BPod Crandom-generator Pod A / B / C 중 하나
클라이언트는 고정 IP 하나만 안다

셀렉터: 생성 순서와 무관한 연결

  • 셀렉터(selector)가 핵심이다
  • Pod가 나중에 생겨도, 먼저 생겨도 라벨만 맞으면 Service가 자동으로 묶어준다
    • Pod 먼저 생성 → Service 나중에 생성 → 라벨 보고 자동 연결 ✅
    • Service 먼저 생성 → Pod 나중에 생성 → 라벨 보고 자동 연결 ✅

3.2 Virtual IP의 실체: kube-proxy와 iptables

ClusterIP: 인터페이스 없는 가상 IP

  • ClusterIP는 실제로 어떤 네트워크 인터페이스에도 존재하지 않는 가상 IP다
  • 그렇다면 패킷이 어떻게 실제 Pod에 도달할까

kube-proxy의 iptables 갱신

  • 모든 노드에서 실행 중인 kube-proxy가 새 Service를 감지한다
  • 그리고 노드의 iptables 규칙을 업데이트한다
노드kube-proxy는 모든 노드에서 실행 중kube-proxy새 Service 감지iptables 규칙 업데이트클라이언트10.109.0.5:80(가상 IP)iptables 규칙 발동"이 IP로 오는 패킷 잡아서 변환"10.108.0.5:8080실제 Pod IP로 교체실제 Pod에 도달
가상 IP는 iptables 규칙 안에서만 존재한다

제약: ClusterIP로 ping 불가 (iptables 모드 기준)

  • iptables는 Service 정의에 명시된 TCP/UDP 프로토콜에 대해서만 규칙을 추가한다
  • 그래서 iptables 모드에서는 ClusterIP로 ping(ICMP)이 불가능하다
    • curl은 되지만 ping은 응답이 없다
  • IPVS 모드에서는 ClusterIP가 노드의 kube-ipvs0 인터페이스에 실제 주소로 붙기 때문에 ping에 응답할 수 있다
    • IPVS 모드는 v1.35부터 deprecated다

가상 IP가 필요한 이유

  • Pod IP는 계속 바뀌지만, 클라이언트에게는 항상 같은 고정 주소를 줘야 하기 때문이다
  • 실제 IP(Pod IP) 직접 사용 시
    • Pod 죽으면 IP 사라짐 ❌
    • 스케일 업 시 새 IP 모름 ❌
  • 가상 IP(ClusterIP) 사용 시
    • Pod가 몇 개든, IP가 바뀌든 ClusterIP는 그대로 ✅
    • kube-proxy가 뒤에서 실제 Pod IP로 변환 ✅

3.3 디스커버리 방법 1: 환경변수

시작 시점의 환경변수 주입

  • Pod가 시작될 때 그 시점까지 존재하는 모든 Service의 정보가 자동으로 환경변수로 주입된다
# Example 13-2. Service-related environment variables
RANDOM_GENERATOR_SERVICE_HOST=10.109.72.32
RANDOM_GENERATOR_SERVICE_PORT=80

앱 코드에서의 사용

  • 앱 코드에서는 이 환경변수를 읽어 Service에 접근할 수 있다
import os, requests

host = os.environ['RANDOM_GENERATOR_SERVICE_HOST']
port = os.environ['RANDOM_GENERATOR_SERVICE_PORT']
requests.get(f"http://{host}:{port}")

장점

  • 어떤 언어로도 환경변수 읽기 가능 (언어 무관)
  • 클러스터 밖에서도 환경변수 직접 설정으로 테스트 가능

단점 — 순서 제약

  • ❌ 잘못된 순서: Pod 먼저 시작 → Service 나중에 생성
    • 환경변수 없음! 영원히 접근 불가 (Pod 재시작 필요)
  • ✅ 올바른 순서: Service 먼저 생성 → Pod 나중에 시작
    • 환경변수 정상 주입

주입 시점: 시작 시 단 한 번

  • 환경변수는 Pod 시작 시점에 딱 한 번 주입된다
  • 이미 실행 중인 Pod에는 나중에 추가할 수 없다

3.4 디스커버리 방법 2: DNS

CoreDNS 자동 등록

  • Kubernetes는 클러스터 내부에 CoreDNS 라는 DNS 서버를 실행한다
  • 새 Service가 생성되면 CoreDNS에 자동으로 등록된다
  • 모든 Pod는 이 DNS 서버를 자동으로 사용하도록 구성된다
Pod에서 random-generator 호출 시Podrandom-generator:80 입력CoreDNSCoreDNS 조회"random-generator = 10.109.0.5 야"실제 Service IP로 연결10.109.0.5Service IP
Pod는 이름만 알고, IP는 CoreDNS가 알려 준다

이름만으로 접근

  • 클라이언트 입장에서는 IP를 몰라도 Service 이름만 알면 된다
# DNS 방식 — 이름만 쓰면 끝
requests.get("http://random-generator:80")

FQDN (완전 정규화 도메인 이름)

  • Service의 전체 DNS 주소는 다음과 같은 형식이다
random-generator①.default②.svc③.cluster.local④
FQDN은 네 부분으로 나뉜다
부분의미
① random-generatorService 이름
② default네임스페이스
③ svcService 리소스 표시
④ cluster.local클러스터 고유 suffix

네임스페이스에 따른 이름 사용

  • 같은 네임스페이스에서는 Service 이름만 써도 된다
  • 다른 네임스페이스에서는 random-generator.default 형식으로 쓰면 된다
  • 어떻게 이게 가능한지, Pod 내부를 들여다보면 그 답이 있다

/etc/resolv.conf: Pod가 DNS를 찾아가는 설정 파일

  • Pod 내부에는 리눅스 표준 DNS 설정 파일인 /etc/resolv.conf가 있다
  • random-generator처럼 짧은 이름만 입력해도 CoreDNS까지 도달할 수 있는 이유가 이 파일 안에 있다
# Pod 내부 /etc/resolv.conf 예시
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
줄의미
nameserver질의를 보낼 DNS 서버 IP — CoreDNS의 ClusterIP
search짧은 이름 뒤에 자동으로 붙여볼 접미사 목록
options ndots:5점(.)이 5개 미만인 이름은 먼저 search 목록으로 시도

파일을 만드는 주체와 읽는 주체

  • 이 파일은 개발자가 손으로 작성하는 게 아니다
    • kubelet이 Pod를 생성할 때 자동으로 만들어 주입한다
  • 앱 코드가 이 파일을 직접 읽는 것도 아니다
    • 리눅스의 DNS resolver(glibc, musl 등)가 이름을 조회할 때마다 OS 레벨에서 알아서 참고하는 표준 메커니즘이다
  • 즉 앱은 requests.get("http://random-generator:80")이라고만 쓰면 된다
    • 그 뒤의 이름 해석은 전부 OS가 이 파일을 보고 처리해준다

search 도메인: 짧은 이름이 FQDN으로 완성되는 과정

  • search 줄에는 접미사 후보가 순서대로 나열되어 있다
  • 짧은 이름을 입력하면 resolver가 이 목록을 하나씩 붙여가며 순서대로 조회를 시도한다
  • Pod에서 “random-generator” 조회 시 시도 순서는 아래와 같다
순서조회 이름조건
1차 시도random-generator.default.svc.cluster.local성공 시 여기서 종료
2차 시도random-generator.svc.cluster.local1차 실패 시에만
3차 시도random-generator.cluster.local2차 실패 시에만

네임스페이스를 붙인 이름

  • random-generator.default처럼 이미 네임스페이스를 포함한 이름을 쓰면 첫 번째 search 접미사(default.svc.cluster.local)만 붙어도 바로 FQDN이 완성된다
  • 결과적으로 짧은 이름과 .default를 붙인 이름 둘 다 정상 동작한다

search 줄이 없을 때

  • 짧은 이름이 접미사 없이 그대로 nameserver(CoreDNS)로 질의된다
  • CoreDNS는 클러스터 도메인(cluster.local)이 아닌 이름을 상위(upstream) DNS로 전달(forward)한다
  • random-generator라는 도메인은 당연히 존재하지 않으므로 조회는 실패한다
  • search 도메인이야말로 “Service 이름 하나만 쓰면 되는” 경험을 가능하게 하는 핵심 장치다

CoreDNS 자신도 하나의 Service다

  • nameserver 10.96.0.10에 박혀 있는 이 IP는 다름 아닌 CoreDNS의 ClusterIP다
  • CoreDNS는 kube-system 네임스페이스에 kube-dns라는 이름의 Service로 떠 있는, 다른 Service들과 동일한 존재다
kube-system 네임스페이스Service: kube-dnsClusterIP: 10.96.0.10← 클러스터 내 모든 Pod의 resolv.conf에 박히는 IPCoreDNS Pod(들)
CoreDNS도 ClusterIP를 가진 평범한 Service다

순환 문제: CoreDNS를 찾아가는 첫걸음

  • 여기서 재밌는 순환 문제가 생긴다
    • “Service 이름을 DNS로 찾으려면 CoreDNS가 필요한데, CoreDNS 자체도 Service라면 그건 어떻게 찾지?”
    • 닭이 먼저냐 달걀이 먼저냐 문제다
  • Kubernetes는 이걸 아주 실용적으로 해결한다
    • CoreDNS의 ClusterIP는 DNS 조회 없이 resolv.conf에 고정 IP로 직접 박아둔다
  • 즉 CoreDNS를 찾아가는 첫걸음만큼은 DNS를 쓰지 않고 우회하는 셈이다

네임스페이스마다 resolv.conf가 조금씩 다르다

  • search 줄의 첫 번째 항목은 Pod가 속한 네임스페이스 이름을 반영해 자동으로 바뀐다
  • 네임스페이스 이름만 다르고 나머지는 동일하다
Pod의 네임스페이스search 줄
defaultsearch default.svc.cluster.local svc.cluster.local cluster.local
paymentsearch payment.svc.cluster.local svc.cluster.local cluster.local

짧은 이름의 해석 범위

  • 이 덕분에 payment 네임스페이스의 Pod가 random-generator라는 짧은 이름을 조회하면, 자동으로 random-generator.payment.svc.cluster.local부터 먼저 시도한다
  • 즉 짧은 이름은 항상 “내가 속한 네임스페이스” 안에서만 자동으로 해석된다

이름 해석 규칙일 뿐, 보안 경계는 아니다

  • 다른 네임스페이스의 Service를 짧은 이름만으로 우연히, 또는 실수로 찾아가는 혼동을 줄인다
  • 정말로 다른 네임스페이스의 Service에 접근하고 싶다면 random-generator.default처럼 네임스페이스를 명시적으로 적어야 한다
  • 짧은 이름이 조용히 다른 팀의 Service로 연결되는 일은 일어나지 않는다
  • 하지만 네임스페이스만 적으면 어느 Pod든 다른 네임스페이스의 Service를 조회하고 접근할 수 있다
    • 네임스페이스 간 접근을 실제로 막는 것은 DNS가 아니라 NetworkPolicy다 (지원하는 네트워크 플러그인 필요)

결론: 맞물린 세 가지 장치

  • 같은 네임스페이스 안에서 Service 이름만으로 접근이 가능한 이유는 우연이 아니다
  • 세 가지 장치가 맞물린 결과다
    • ① kubelet이 자동 생성한 resolv.conf
    • ② 그 안의 search 도메인이 붙여주는 네임스페이스 접미사
    • ③ CoreDNS가 고정 IP로 항상 먼저 접근 가능하다는 점

레퍼런스

이 동작은 Kubernetes 공식 문서 "DNS for Services and Pods"의 "Namespaces of Services" 섹션에 명시되어 있다.
"DNS queries may be expanded using the Pod's /etc/resolv.conf. kubelet configures this file for each Pod. For example, a query for just data may be expanded to data.test.svc.cluster.local. The values of the search option are used to expand queries."
[해석] "DNS 조회는 Pod의 /etc/resolv.conf를 통해 확장될 수 있다. kubelet이 각 Pod마다 이 파일을 설정해준다. 예를 들어 data라는 짧은 이름만으로 조회해도 data.test.svc.cluster.local로 자동 확장될 수 있다. search 옵션의 값들이 이 조회 확장에 사용된다."
그리고 바로 아래 예시로 제시된 resolv.conf 블록:
nameserver 10.32.0.10
search <namespace>.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
이 두 부분이 "Service 이름만 써도 되는" 경험의 공식 근거다.
→ kubernetes.io — DNS for Services and Pods #namespaces-of-services

환경변수 방식과의 결정적 차이

방식결정 시점Service 생성 순서
환경변수Pod 시작 시점에 결정Service가 먼저 있어야 함
DNS요청 시점마다 조회나중에 Service 생겨도 그때 찾으면 됨

DNS 방식에서 Pod가 먼저 떠도 되는 이유

  • 요청을 보낼 때 그때그때 CoreDNS에 물어보기 때문이다
  • Service가 나중에 생성되면 CoreDNS에 등록되고, 그 이후 Pod의 조회는 성공한다

보완: 비표준 포트

  • 비표준 포트는 DNS로 알 수 없다
  • http://random-generator:80 처럼 표준 포트라면 그냥 쓰면 된다
  • 포트가 비표준이라면 환경변수(RANDOM_GENERATOR_SERVICE_PORT)를 읽어 보완한다

3.5 ClusterIP 기타 특성

다중 포트 (Multiple Ports)

  • Service 하나에 여러 포트를 동시에 정의할 수 있다
  • HTTP/HTTPS를 모두 지원하는 Pod에 Service 두 개를 만들 필요가 없다
ports:
  - name: http
    port: 80
    targetPort: 8080
  - name: https
    port: 443
    targetPort: 8443

세션 어피니티 (Session Affinity)

  • 기본적으로 Service는 요청마다 랜덤으로 Pod를 선택한다
    • 요청1 → Pod A / 요청2 → Pod C / 요청3 → Pod B (랜덤)
  • sessionAffinity: ClientIP를 설정하면 같은 클라이언트 IP에서 오는 요청은 항상 같은 Pod로 전달된다
    • 10.0.0.1의 모든 요청 → 항상 Pod A ✅

한계: L4 로드밸런싱

  • 단, Service는 L4(Transport Layer) 로드밸런싱을 수행한다
  • IP와 포트만 보며, HTTP 쿠키 같은 애플리케이션 레벨 정보는 볼 수 없다
  • 쿠키 기반 세션 고정이 필요하다면 Ingress를 써야 한다

Readiness Probe 연동

  • Pod가 readinessProbe를 정의했고 체크가 실패하고 있다면, 라벨이 맞더라도 Service 엔드포인트 목록에서 자동으로 제외된다
    • Pod A (준비됨) ✅ → 트래픽 받음
    • Pod B (준비중) ❌ → 트래픽 차단 (라벨 맞아도!)
  • 준비된 Pod에만 트래픽이 간다

ClusterIP 직접 지정

  • 기본적으로 자동 할당되지만, .spec.clusterIP 필드로 직접 지정할 수 있다
  • 레거시 앱이 특정 IP를 하드코딩하거나 기존 DNS 항목을 재사용해야 할 때 유용하다
  • 단, 권장하지 않는다

4. Manual Service Discovery

수동 엔드포인트가 필요한 대상

  • 일반 Service는 셀렉터로 Pod를 자동으로 찾아 엔드포인트를 관리한다
  • 하지만 외부 DB, 레거시 시스템처럼 Pod가 아닌 대상에 연결할 때는 엔드포인트를 수동으로 지정한다

4.1 Selector 없는 Service + 수동 Endpoint

Step 1. 셀렉터 없는 Service 생성

# Example 13-3. Service without selector
apiVersion: v1
kind: Service
metadata:
  name: external-service      # ① Endpoint와 이름을 맞출 기준 — DNS 이름으로도 사용됨
spec:
  type: ClusterIP             # ② 내부 전용 가상 IP만 발급 (기본값이라 생략 가능)
  ports:
    - protocol: TCP
      port: 80                # ③ 내부 Pod가 접근할 포트
  # selector 없음 — ④ Kubernetes야 Pod 찾지 마, 내가 직접 지정할게

Step 2. 같은 이름으로 Endpoint 수동 생성

# Example 13-4. Endpoints for an external service
apiVersion: v1
kind: Endpoints
metadata:
  name: external-service   # ① Service 이름과 반드시 같아야 연결됨!
subsets:
  - addresses:
    - ip: 1.1.1.1          # ② 외부 서버 1 — 실제 라우팅 대상
    - ip: 2.2.2.2          # ③ 외부 서버 2 — 여러 개면 자동 로드밸런싱
    ports:
    - port: 8080           # ④ 외부 서버가 실제로 듣는 포트 (Service의 port와 별개)

이름 기반 매칭

  • Kubernetes는 Service와 Endpoint를 이름으로 매칭한다
  • 이름이 같아야 Service가 해당 Endpoint를 사용한다
클러스터내부 Podexternal-serviceClusterIP (가상IP)Endpoint 리소스(수동 지정)외부 서버1.1.1.1:80802.2.2.2:8080또는 (로드밸런싱)
Service 이름 뒤에 외부 서버 IP를 손으로 붙인다

소비자 입장의 투명성

  • 내부 Pod 입장에서는 외부 서버든 내부 Pod든 Service 이름으로 똑같이 접근한다
  • 외부 서버 IP가 바뀌어도 Endpoint만 수정하면 되고, 코드는 건드릴 필요가 없다

주의: Endpoint에 넣을 수 없는 주소

  • Endpoint에는 일반 Pod IP나 외부 IP는 넣을 수 있다
  • 하지만 다른 Service의 ClusterIP(가상 IP)는 넣을 수 없다
    • ClusterIP는 iptables에서만 처리되는 주소라 Endpoint에서는 라우팅이 불가하다

마이그레이션 활용

  • 이 패턴의 가장 강력한 활용처는 온프레미스 → Kubernetes 마이그레이션이다
1단계: 온프레미스 서버 운영 중클라이언트Service IP(고정)Endpoint외부 서버 IP 직접 지정외부 서버온프레미스2단계: Kubernetes로 이전 완료클라이언트Service IP(고정)selector 추가Endpoint수동 관리 → 자동 관리로 전환Pod클라이언트는 Service IP가 그대로라 아무것도 바꿀 필요 없음 ✅
Service IP는 그대로 두고 뒤쪽만 갈아 끼운다

핵심: Service는 유지

  • Service를 삭제 후 재생성하면 ClusterIP가 바뀌어버린다
  • 그래서 Service는 유지하면서 selector와 Endpoint만 교체하는 것이 핵심이다

4.2 ExternalName

도메인으로 제공되는 외부 서비스

  • 외부 서비스가 IP가 아닌 도메인 이름으로 제공될 때 사용한다
# Example 13-5. Service with an external destination
apiVersion: v1
kind: Service
metadata:
  name: database-service            # ① 내부 Pod가 사용할 이름 — 실제 도메인을 감춤
spec:
  type: ExternalName                # ② ClusterIP 발급 없이 DNS CNAME으로만 동작
  externalName: my.database.example.com   # ③ 실제 리다이렉트될 외부 도메인
  ports:
    - port: 80                      # ④ ExternalName에서는 사실상 참고용 (프록시하지 않음)

동작 방식: DNS CNAME

  • 동작 방식은 Manual Endpoint와 다르다
  • IP가 아니라 DNS CNAME으로 동작한다
Manual Endpoint 방식(프록시 거침)PodClusterIPkube-proxy외부 IPExternalName 방식(프록시 없음)PodDNS 조회my.database.example.comCNAME직접 연결(프록시 없음)
IP로 프록시하느냐, 이름만 바꿔 주느냐

CNAME 리다이렉트

  • database-service.default.svc.cluster.local을 조회하면 my.database.example.com으로 CNAME 리다이렉트된다
  • Pod는 Service 이름만 알면 된다
  • 실제 외부 도메인이 바뀌어도 Service 정의만 수정하면 된다

도메인 직접 사용과의 차이

  • 그냥 도메인을 코드에 직접 써도 동작한다
  • 하지만 ExternalName을 쓰면 환경마다 다른 도메인을 Service 이름 하나로 추상화할 수 있다
  • 코드는 항상 “database-service"만 쓴다 ✅
환경database-service가 가리키는 도메인
개발환경dev.database.example.com
운영환경prod.database.example.com

5. External Service Discovery

반대 방향: 외부 → 클러스터 내부

  • 지금까지는 클러스터 내부에서의 디스커버리였다
  • 이제 반대 방향, 외부에서 클러스터 안의 Pod에 접근하는 방법을 다룬다

외부 진입점의 필요성

  • ClusterIP는 클러스터 내부 가상 네트워크에만 존재하는 IP라, 외부 인터넷에서는 라우팅 자체가 불가능하다
  • 외부 진입점을 따로 만들어야 한다

5.1 NodePort

모든 노드의 포트 개방

  • 클러스터의 모든 노드에 특정 포트를 열어 외부에서 접근하게 하는 방식이다
# Example 13-6. Service with type NodePort
apiVersion: v1
kind: Service
metadata:
  name: random-generator
spec:
  type: NodePort           # ① ClusterIP + 모든 노드의 포트 개방을 함께 생성
  selector:
    app: random-generator  # ② 이 라벨을 가진 Pod로 트래픽 전달
  ports:
    - port: 80          # ③ ClusterIP 포트 (내부용) — 클러스터 내부에서는 여전히 이 포트로 접근
      targetPort: 8080  # ④ 실제 Pod가 듣는 포트
      nodePort: 30036   # ⑤ 모든 노드에 열리는 외부 포트 (30000~32767 범위, 생략 시 자동 할당)
      protocol: TCP     # ⑥ TCP 기본, NodePort는 UDP도 지원

포트 흐름

클러스터외부 사용자노드IP:30036(nodePort)ClusterIP:80(port) ← 자동 생성Pod:8080(targetPort)
nodePort → port → targetPort

ClusterIP 위에 올라타는 구조

  • NodePort는 ClusterIP 위에 올라타는 구조다
  • NodePort Service를 만들면 ClusterIP도 자동 생성된다
    • 내부에서 접근: ClusterIP:80 → Pod:8080
    • 외부에서 접근: 노드IP:30036 → ClusterIP:80 → Pod:8080

모든 노드에 동시에 열린다

  • Pod가 없는 노드로 요청이 와도, kube-proxy가 다른 노드의 Pod로 전달해준다
노드노드 IP30036 포트로 접근
노드A192.168.1.10192.168.1.10:30036 → Pod 접근 ✅
노드B192.168.1.11192.168.1.11:30036 → Pod 접근 ✅ (Pod가 그 노드에 없어도!)

단점들

단점설명
포트 번호30000~32767 사이의 못생긴 포트 번호
방화벽클라우드에서 해당 포트를 방화벽에서 열어줘야 함
노드 장애클라이언트가 죽은 노드로 접속하면 연결 실패 (클라이언트가 직접 재시도해야 함)

Source NAT 문제

  • 외부 클라이언트 요청이 한 노드에 도착했는데 Pod가 다른 노드에 있는 경우다
  • 노드 간 패킷 전달 과정에서 출발지 IP가 노드 IP로 교체된다
요청은 노드A에 도착했는데 Pod는 노드B에 있을 때클라이언트99.99.99.99노드A노드A:30036노드A가 패킷 수정출발지 IP: 99.99.99.99→ 노드A IP로 변경!노드B(Pod가 노드B에 있음)Pod출발지 = 노드A IP→ Pod는 진짜 클라이언트 IP를 알 수 없음 ❌
노드를 한 번 건너면 출발지 IP가 바뀐다

해법: externalTrafficPolicy: Local

  • externalTrafficPolicy: Local을 설정하면 항상 같은 노드의 Pod로만 전달해 Source NAT가 발생하지 않는다
  • 단, 해당 노드에 Pod가 없으면 연결 실패다
  • DaemonSet으로 모든 노드에 Pod를 배치하는 경우에 유용하다

5.2 LoadBalancer

클라우드 로드밸런서 연동

  • NodePort의 한계를 해결하는 방식이다
  • NodePort 위에 클라우드 제공자의 로드밸런서를 자동으로 붙인다
# Example 13-7. Service of type LoadBalancer
apiVersion: v1
kind: Service
metadata:
  name: random-generator
spec:
  type: LoadBalancer             # ① NodePort + ClusterIP + 클라우드 LB까지 한 번에 생성
  clusterIP: 10.0.171.239        # ② Kubernetes가 자동 할당 (내부 전용, 직접 지정 비권장)
  loadBalancerIP: 78.11.24.19    # ③ 원하는 외부IP 지정 (선택 사항, 클라우드 지원 시에만 유효)
  selector:
    app: random-generator        # ④ 트래픽을 전달할 Pod 라벨
  ports:
    - port: 80                   # ⑤ 로드밸런서·ClusterIP 공통 포트
      targetPort: 8080           # ⑥ 실제 Pod가 듣는 포트
status:
  loadBalancer:
    ingress:
      - ip: 146.148.47.155       # ⑦ 실제로 생성된 외부IP — Kubernetes가 결과를 자동으로 채워줌

계층 구조

  • LoadBalancer Service 하나만 만들면 아래의 모든 레이어가 자동으로 생성된다
LoadBalancer(클라우드 로드밸런서)NodePort(자동 생성)ClusterIP(자동 생성)Pod
LoadBalancer 하나가 아래 레이어를 모두 만든다

요청 경로

  1. 외부 클라이언트
  2. 클라우드 로드밸런서 (고정 외부IP)
  3. 노드:NodePort (살아있는 노드 자동 선택)
  4. ClusterIP
  5. Pod

클라이언트가 알아야 할 주소

  • 클라이언트는 로드밸런서 IP 하나만 알면 된다
  • 노드가 죽어도, Pod가 이동해도, 클라이언트는 아무것도 바꿀 필요가 없다

YAML의 IP 3개 이해하기

필드의미누가
spec.clusterIP클러스터 내부 가상IPKubernetes 자동 할당
spec.loadBalancerIP원하는 외부IP (선택)개발자가 미리 예약
status.loadBalancer.ingress[].ip실제로 생성된 외부IPKubernetes가 결과를 여기에 기록

spec과 status의 역할

  • spec은 내가 원하는 것을 Kubernetes에 요청하는 곳이다
  • status는 Kubernetes가 실제 결과를 알려주는 곳이다
  • 개발자는 status.loadBalancer.ingress[].ip를 확인해 DNS에 등록하거나 외부에 공개한다

단점: Service마다 로드밸런서 비용

  • Service 하나당 클라우드 로드밸런서 하나가 생성된다
  • Service가 10개면 로드밸런서도 10개, 그만큼 비용이 발생한다
  • 이 문제를 해결하는 것이 Ingress다

클라우드 제공자 의존

  • 이 Service 타입은 클라우드 제공자가 Kubernetes를 지원할 때만 동작한다
  • 세부 동작(IP 고정 가능 여부, Source IP 보존 여부 등)은 AWS, GCP, DigitalOcean마다 다르다
    • 각 클라우드 문서를 확인해야 한다

5.3 Ingress

Service 앞의 별도 리소스

  • Ingress는 Service 타입이 아니라 별도의 Kubernetes 리소스다
  • Service들 앞에 위치해 스마트 라우터 역할을 한다

핵심 강점: 외부IP 하나로 여러 Service 처리

LoadBalancer 방식외부IP 1개 💰결제 Service외부IP 1개 💰주문 Service외부IP 1개 💰유저 ServiceIngress 방식외부IP 1개/payment결제 Service ✅/order주문 Service ✅/user유저 Service ✅
Service마다 외부IP를 쓰느냐, 외부IP 하나로 나누느냐

L7 라우팅

  • Ingress는 HTTP URL 경로까지 보는 L7(Application Layer) 라우팅을 수행한다
  • NodePort, LoadBalancer가 IP/포트만 보는 L4인 것과 차이가 있다

기본 Ingress 정의

# Example 13-8. An Ingress definition
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: random-generator
spec:
  defaultBackend:               # ① 별도 rules가 없을 때 모든 요청이 향하는 기본 목적지
    service:
      name: random-generator    # ② 기본 목적지 Service 이름
      port:
        number: 8080            # ③ Service가 노출하는 포트 (targetPort 아님)

포트 번호 주의

  • 책의 예제(13-8, 13-9)는 number: 8080으로 적혀 있지만, 3.1의 Service는 port: 80이므로 이 글의 Service 기준으로는 number: 80이 맞다

defaultBackend만 쓸 때

  • defaultBackend만 쓰면 모든 요청이 하나의 Service로 간다
  • 이건 LoadBalancer와 큰 차이가 없다
  • 진짜 강점은 다음의 경로 기반 라우팅이다

Fan-out (경로 기반 라우팅)

# Example 13-9. A definition for Nginx Ingress controller
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: random-generator
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /   # ① Controller별 추가 설정 — nginx 전용 네임스페이스 접두사
spec:
  rules:                          # ② 경로별 라우팅 규칙 목록
  - http:
      paths:
      - path: /                   # ③ 매칭 경로
        pathType: Prefix          # ④ /로 시작하는 모든 경로 매칭 (/hello, /foo/bar 등)
        backend:
          service:
            name: random-generator   # ⑤ 이 경로로 온 요청이 향할 Service
            port:
              number: 8080
      - path: /cluster-status     # ⑥ 두 번째 라우팅 규칙 — 별도 Service로 분기
        pathType: Exact           # ⑦ 정확히 /cluster-status만 매칭 (하위 경로 불포함)
        backend:
          service:
            name: cluster-status
            port:
              number: 80

pathType 차이

pathType예시 경로매칭 범위
Prefix/abc/abc, /abc/def, /abc/xyz 전부 매칭
Exact/abc정확히 /abc만 매칭 (/abc/def는 안됨)

라우팅 동작

요청전달 대상
외부IP:80/random-generator:80
외부IP:80/hellorandom-generator:80 (Prefix 매칭)
외부IP:80/cluster-statuscluster-status:80

Ingress Controller 필수

  • Ingress 리소스는 설계도일 뿐이다
  • 실제로 동작하려면 클러스터에 Ingress Controller가 설치되어 있어야 한다
    • Ingress 리소스 = 설계도 (무엇을 라우팅할지)
    • Ingress Controller = 실제 엔진 (어떻게 라우팅할지)

대표적인 Controller

Controller특징
nginx-ingress가장 널리 사용
traefik동적 설정, 자동 TLS
AWS ALB IngressAWS 네이티브 ALB 연동

Controller별 추가 설정

  • Controller마다 추가 설정 방식이 다르다
  • nginx라면 annotations에 nginx.ingress.kubernetes.io/ 접두사 설정을 넣는다
  • traefik이라면 traefik 전용 설정을 쓴다

Ingress Controller도 Pod다

  • Ingress Controller는 클러스터 내부에서 Pod로 실행된다
  • 대부분의 Controller(예: ingress-nginx)는 Ingress에 적힌 Service(random-generator)의 Endpoints/EndpointSlice를 API 서버에서 지켜본다
  • 그래서 요청을 ClusterIP를 거치지 않고 Pod IP로 바로 전달한다
    • ingress-nginx는 nginx.ingress.kubernetes.io/service-upstream 설정으로 ClusterIP를 거치게 바꿀 수 있다
외부클러스터 내부Ingress Controller(Pod, 클러스터 내부)Pod10.244.1.5:8080Pod10.244.2.7:8080EndpointSlicerandom-generator의 Pod IP 목록Pod IP로 바로 전달감시ClusterIP(kube-proxy)는 거치지 않는다
Ingress Controller는 EndpointSlice를 보고 Pod로 바로 보낸다

Ingress 제공 기능

  • HTTP/HTTPS 기반 URL 경로 라우팅
  • 도메인 기반 가상 호스팅 (payment.myapp.com, order.myapp.com)
  • TLS 종료 (HTTPS 처리)
  • 로드밸런싱

한계: 비HTTP 프로토콜

  • 단, HTTP/HTTPS 외의 프로토콜(TCP, UDP)에는 제한적이다
  • 비HTTP 트래픽은 NodePort나 LoadBalancer가 더 적합하다

6. Discussion

전체 메커니즘 정리

이름설정방향용도
ClusterIPtype: ClusterIP내부가장 일반적인 내부 디스커버리
HeadlessclusterIP: None내부가상IP 없이 Pod 직접 DNS 접근 (StatefulSet)
Manual IPkind: Endpoints내부→외부외부 IP 직접 지정
ExternalNametype: ExternalName내부→외부외부 도메인 CNAME 연결
NodePorttype: NodePort외부→내부비HTTP 트래픽, 단순 외부 노출
LoadBalancertype: LoadBalancer외부→내부클라우드 로드밸런서 자동 연동
Ingresskind: Ingress외부→내부HTTP 기반 스마트 라우팅, 비용 절감

단순 → 복잡 순서

  1. ClusterIP — 가장 기본, 내부 전용
  2. NodePort — 외부 노출 기본
  3. LoadBalancer — NodePort + 클라우드 로드밸런서
  4. Ingress — L7 라우팅, 가장 강력하고 복잡

핵심 메시지

플랫폼이 대신 처리하는 일

  • 개발자가 직접 해야 했던 것들을 전부 Kubernetes가 자동으로 처리한다
    • Pod IP 추적, DNS 등록, 로드밸런싱, 노드 장애 처리
  • 개발자는 YAML만 작성하면 된다

Service의 역할과 메커니즘 선택 기준

  • Service는 Pod IP(계속 변함)와 클라이언트 사이의 안정적인 추상화 레이어다
  • 어떤 메커니즘을 선택하느냐는 소비자와 제공자의 위치(내부/외부)에 따라 결정된다

Service Discovery는 “Pod를 어떻게 안정적으로 찾아가느냐” 의 문제다. Kubernetes는 클러스터 내부의 ClusterIP/DNS부터 외부 노출을 위한 NodePort/LoadBalancer/Ingress까지, 상황에 맞는 다양한 메커니즘을 제공해 이 문제를 플랫폼 레벨에서 해결한다.