Service
- 공식 문서 - Service
- 클러스터 안에서 변동하는 Pod 집합을 안정적으로 노출하는 네트워크 추상화
- 같은 Pod 라벨을 가진 그룹에 대한 단일 진입점(VIP)을 제공하고 트래픽을 분배
- Deployment에서 만든 Pod들을 실제로 호출 가능한 대상으로 만들어주는 다음 단계
Service가 필요한 이유
- Pod는 수명이 짧고 IP가 변한다
- Deployment가 desired state를 맞추는 과정에서 Pod가 만들어지고 사라짐
- 같은 이름이라도 재생성되면 IP가 달라짐
- 클라이언트가 직접 Pod IP로 접근하면 매번 끊기고 재발견해야 함
- Service가 그 사이에서 다음을 추상화
- 고정 엔드포인트 — 클러스터 내부 가상 IP(ClusterIP)는 Pod가 바뀌어도 유지됨
- 로드밸런싱 — 매칭되는 Pod들 사이로 트래픽을 분배
- 서비스 디스커버리 — Pod IP를 몰라도 Service 이름으로 접근
- 디커플링 — 클라이언트는 “어떤 Pod”가 아니라 “어떤 Service”에 의존
- 예: 이미지 처리 backend가 3개 replica로 떠 있을 때, frontend는 backend Pod 목록을 추적하지 않고 backend Service만 호출
Service 정의
- 핵심은
selector로 Pod를 묶고, ports로 노출 규칙을 선언하는 것
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app.kubernetes.io/name: MyApp # 이 라벨을 가진 Pod 모두를 이 Service로 묶음
ports:
- protocol: TCP
port: 80 # Service가 노출하는 포트 (클라이언트 접속 대상)
targetPort: 9376 # Pod 컨테이너가 listen 중인 포트
spec.selector
- Service가 트래픽을 보낼 Pod를 식별하는 라벨 셀렉터
selector가 있으면 쿠버네티스가 매칭되는 Pod로 EndpointSlice를 자동 생성·갱신
- 라벨 매칭 결과가 비면 트래픽 받을 대상이 없는 Service가 됨 (정상 동작이지만 사실상 죽은 상태)
spec.ports — port vs targetPort
port — Service 자체가 노출하는 포트. 클라이언트가 접속하는 포트 (ClusterIP가 listen)
targetPort — Pod 컨테이너가 listen 중인 포트
- 둘은 같지 않아도 된다 — 외부 인터페이스(
port)와 내부 구현(targetPort)을 분리할 수 있음
targetPort는 숫자 대신 컨테이너의 포트 이름을 써도 됨 (Pod의 containers.ports.name 참조)
protocol과 appProtocol
protocol (기본 TCP) — TCP / UDP / SCTP 중 하나
appProtocol (선택) — 애플리케이션 레벨 프로토콜 힌트 (HTTP, HTTPS, gRPC 등)
- Ingress 컨트롤러나 Service Mesh가 라우팅·관측에 활용
Multi-port Service
- 하나의 Service로 여러 포트를 동시에 노출 가능
- 각 포트마다
name이 반드시 있어야 한다 (단일 포트일 땐 생략 가능)
```yaml
spec:
selector:
app.kubernetes.io/name: MyApp
ports:
- name: http # multi-port에서는 name 필수
protocol: TCP
port: 80
targetPort: 8080
- name: https
protocol: TCP
port: 443
targetPort: 8443
- name: dns
protocol: UDP
port: 53
targetPort: 5353
```
- 같은 Pod의 다른 포트(예: 메트릭, gRPC, HTTP)를 한 Service로 묶을 때 자주 사용
Selector 없는 Service
selector를 지정하지 않으면 쿠버네티스가 EndpointSlice를 자동 생성하지 않음
- 사용자가 EndpointSlice를 직접 만들어 Service의 백엔드 주소를 지정
- 활용 사례
- 클러스터 외부 서비스(레거시 DB·외부 API)를 Service 이름으로 호출하고 싶을 때
- 다른 네임스페이스의 Service로 우회 라우팅
- 마이그레이션 중 외부와 내부를 동시에 가리키는 추상 엔드포인트가 필요할 때
- 정의 예
```yaml
Service — selector 없음
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9376
—
사용자가 직접 만드는 EndpointSlice
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: my-service-1
labels:
kubernetes.io/service-name: my-service # 어떤 Service에 붙는지 지정
addressType: IPv4
ports:
- name: “”
port: 9376
endpoints:
- addresses: [“10.1.2.3”] # 외부 서버 IP 등
```
- 핵심 제약 — EndpointSlice의
kubernetes.io/service-name 라벨이 Service 이름과 일치해야 연결됨
- 백엔드가 바뀌면 사용자가 직접 EndpointSlice를 갱신해야 한다
EndpointSlice 메커니즘
- Service가 트래픽을 흘려보낼 실제 백엔드 주소들의 목록을 담는 오브젝트
apiVersion: discovery.k8s.io/v1
- 동작 흐름
- Service에
selector가 있으면 쿠버네티스가 매칭 Pod로 EndpointSlice를 자동 생성
- Pod가 추가·삭제되면 EndpointSlice를 자동 갱신
- 각 노드의 kube-proxy가 EndpointSlice를 watch하고 iptables·IPVS 같은 네트워크 규칙을 갱신
- 결과적으로 ClusterIP로 들어온 트래픽이 살아 있는 Pod로 분배됨
- 슬라이스 단위로 분할 — 슬라이스 하나당 약 100개 엔드포인트
- 큰 클러스터에서도 watch 부하·etcd 부하를 분산할 수 있음
- 이전의
Endpoints 오브젝트(단일 객체에 모든 백엔드 저장)를 대체하기 위해 도입 — 대규모 클러스터에서 확장성을 확보
서비스 디스커버리(Service Discovery)
- Pod가 Service에 도달하는 방법은 크게 두 가지 — 환경 변수와 DNS
- 실무에서는 거의 항상 DNS 방식을 사용하고, 환경 변수 방식은 한계 때문에 보조적
- 어떤 방식을 쓰든 클라이언트는 Pod IP가 아니라 Service 이름/ClusterIP로만 접근
환경 변수(Environment Variables) 방식
- kubelet이 Pod를 띄울 때, 그 시점에 이미 존재하는 Service들에 대해 환경 변수를 자동 주입
- 이름 규칙은 Service 이름을 대문자·언더스코어로 변환한 형태
- ex) Service 이름
redis-primary → REDIS_PRIMARY_SERVICE_HOST, REDIS_PRIMARY_SERVICE_PORT
- 동시에 도커 호환 변수도 함께 주입 —
REDIS_PRIMARY_PORT, REDIS_PRIMARY_PORT_6379_TCP 등
- 순서 의존성이 결정적인 한계
- Pod가 생성되기 전에 Service가 존재해야 환경 변수가 주입됨
- Pod 기동 후 새로 만들어진 Service는 그 Pod의 환경 변수에 반영되지 않음
- 따라서 디플로이 순서가 꼬이면 클라이언트가 backend를 못 찾는 문제가 생김
- 대안이 DNS이며, DNS는 런타임에 조회되므로 순서에 영향을 받지 않음
DNS 방식
- 클러스터에 CoreDNS(애드온)가 떠 있으면, 모든 Service는 자동으로 DNS 이름을 갖는다
- Pod의
/etc/resolv.conf가 CoreDNS를 가리키도록 kubelet이 설정 → Service 이름 조회 가능
- FQDN 형식 —
<service-name>.<namespace>.svc.<cluster-domain>
- ex) 기본 도메인이
cluster.local이면 my-service.my-ns.svc.cluster.local
- 같은 네임스페이스에서는 짧은 이름으로도 접근 —
my-service만 써도 됨
search 도메인이 자동 설정되어 있어 my-service → my-service.<my-ns>.svc.cluster.local 순으로 조회
- 다른 네임스페이스에서는
my-service.other-ns처럼 네임스페이스까지 명시
- Service 종류별 레코드
- 일반 Service — Service 이름을 ClusterIP로 해석하는 A/AAAA 레코드 1개
- Headless Service — Pod IP들을 직접 가리키는 A/AAAA 레코드 여러 개
- Named port —
_<port-name>._<protocol>.<service> 형태의 SRV 레코드 제공
- ex)
_http._tcp.my-service.my-ns.svc.cluster.local
CoreDNS가 없는 클러스터에서는 DNS 디스커버리 자체가 동작하지 않음 — 사실상 모든 표준 클러스터는 CoreDNS를 기본 탑재.
Headless Service
spec.clusterIP: None으로 지정한 Service — VIP(ClusterIP)를 할당하지 않는 Service
- 로드밸런싱이나 단일 진입점이 필요 없을 때, Pod들의 실제 주소를 그대로 노출하기 위해 사용
- 정의 예
apiVersion: v1
kind: Service
metadata:
name: my-headless
spec:
clusterIP: None # Headless로 만드는 핵심 필드
selector:
app.kubernetes.io/name: MyApp
ports:
- port: 80
targetPort: 9376
- 일반 Service와의 차이
- ClusterIP 없음 — kube-proxy가 트래픽을 분배하지 않음, iptables 규칙도 만들지 않음
- DNS 응답이 다름 — Service 이름을 조회하면 단일 VIP가 아니라 매칭된 Pod IP 목록이 반환됨
- 클라이언트가 직접 어떤 Pod로 갈지 결정하거나, 클라이언트 측 로드밸런싱을 적용
selector 유무에 따른 동작
selector 있음 — 매칭 Pod로 EndpointSlice가 자동 생성, 각 Pod IP에 대한 A 레코드 발행
selector 없음 — 사용자가 만든 EndpointSlice의 주소들을 그대로 DNS로 노출 (또는 ExternalName CNAME으로 위임)
언제 Headless를 쓰는가
- Stateful한 워크로드 — Pod 각각이 고유 신원(IP·이름)을 가져야 할 때
- ex) Cassandra, Kafka, Zookeeper처럼 노드끼리 직접 통신하는 클러스터드 시스템
- 클라이언트 측 로드밸런싱 — gRPC처럼 connection 단위로 LB가 잘 동작하지 않는 프로토콜
- 클라이언트가 Pod IP 목록을 직접 받아 자체적으로 분산
- 서비스 디스커버리만 필요할 때 — VIP를 통한 트래픽 라우팅은 필요 없고, 멤버 목록만 알고 싶을 때
StatefulSet과의 관계
- StatefulSet은 거의 항상 Headless Service와 짝으로 사용
- StatefulSet의
spec.serviceName이 Headless Service를 가리키면, 각 Pod에 대해 안정적인 DNS 이름이 생성됨
- 형식 —
<pod-name>.<service-name>.<namespace>.svc.<cluster-domain>
- ex)
web-0.nginx.default.svc.cluster.local, web-1.nginx.default.svc.cluster.local
- Pod가 재생성되어 IP가 바뀌어도 Pod 이름 기반의 DNS는 그대로 유지됨
- 분산 시스템의 멤버십·리더 선출·복제 토폴로지에 안정적인 식별자를 제공