TL;DR
- 파드 IP는 교체 시 바뀌므로 직접 사용 불가 → Service가 고정 IP와 DNS 제공
- ClusterIP(내부 통신), LoadBalancer(외부 유입), ExternalName(외부 연결) 등 서비스 타입별 용도 구분
- 서비스 생성 시 DNS 등록 → Endpoints 연결 → kube-proxy가 iptables로 실제 파드에 라우팅
- DNS가 엔드포인트 IP가 아닌 ClusterIP를 반환하는 이유: 파드가 바뀌어도 캐시를 유지하기 위함
1. 왜 Service가 필요한가
이전 글에서 파드와 디플로이먼트를 다뤘다. 파드는 실행할 수 있고, 디플로이먼트가 파드를 관리해준다. 그런데 파드끼리 통신하려면 어떻게 해야 할까?
파드 IP의 한계
파드는 k8s가 부여한 자신만의 가상 IP 주소를 가진다. 이 IP로 다른 노드의 파드와도 통신할 수 있다.
# 파드의 현재 IP 주소 확인
kubectl get pod -l app=sleep-2 --output jsonpath='{.items[0].status.podIP}'하지만 파드는 교체될 때마다 IP가 바뀐다. 디플로이먼트가 파드를 재생성하면 새 IP가 할당되고, 이전 IP는 더 이상 유효하지 않다. 파드 IP를 하드코딩해서 통신하면 파드가 교체될 때마다 연결이 끊어진다.
IP 대신 이름으로 찾기: DNS
IP가 계속 바뀌는 문제를 해결하려면 DNS가 필요하다. 이름으로 조회하면 현재 유효한 IP를 알려주는 방식이다.
k8s 클러스터에는 전용 DNS 서버가 내장되어 있다. 서비스를 생성하면 서비스 이름이 DNS에 등록되고, 파드는 이 이름으로 다른 파드에 접근할 수 있다.
이것이 Service가 존재하는 이유다. Service는 파드 집합 앞에 놓이는 고정된 네트워크 접점으로, 고유 IP 주소를 갖고 서비스가 삭제될 때까지 불변이다.
2. k8s 네트워크의 기본: Service와 L4
Service는 L4에서 동작한다
k8s Service는 L4 전송 계층 기준으로 동작한다. Service가 다루는 프로토콜은 TCP와 UDP 두 가지다.
- TCP: 연결 지향, 순서 보장, 재전송, 신뢰성 제공. 웹 서버, API, DB, gRPC 등 대부분의 애플리케이션에서 사용
- UDP: 비연결형, 재전송/순서 보장 없음, 오버헤드 적고 빠름. DNS, 실시간 통신, 게임/스트리밍에서 사용
Service YAML에서 protocol 필드로 지정할 수 있다. 생략하면 기본값은 TCP다.
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP # 기본값, 생략 가능
- port: 53
targetPort: 53
protocol: UDP # DNS처럼 UDP가 필요한 경우 명시HTTP는 Service가 아닌 Ingress
k8s Service는 HTTP 자체를 이해하지 못한다. Service 입장에서 HTTP 요청은 그냥 TCP 트래픽이다.
- HTTPS는 L7 애플리케이션 계층 프로토콜이고 TCP 위에서 동작한다
- HTTP host/path 기반 라우팅이 필요하면 Service가 아니라 Ingress 같은 L7 계층을 사용해야 한다
Service의 IP는 가상이다
Service의 IP(ClusterIP 등)는 실제 물리적 인터페이스에 할당된 게 아니다. 내부적으로 kube-proxy가 iptables/IPVS 등을 통해 TCP/UDP 트래픽을 가로채서 원하는 Pod로 라우팅한다.
3. 서비스 생성 시 일어나는 일
서비스를 하나 만들면 k8s 내부에서 여러 가지가 자동으로 연결된다. 순서대로 살펴보자.
1단계: DNS에 서비스 이름 등록
서비스 이름이 클러스터 내부 DNS에 등록된다. 형식은 <서비스이름>.<네임스페이스>.svc.cluster.local이다.
# 파드 내부에서 nslookup으로 서비스의 ClusterIP 확인
kubectl exec deploy/sleep-1 -- nslookup sleep-2같은 네임스페이스에서는 서비스 이름만으로 접근 가능하다. (sleep-2만 써도 된다)
2단계: selector와 일치하는 파드가 Endpoints에 등록
서비스 spec의 selector와 일치하는 파드들의 IP와 포트가 자동으로 Endpoints 리소스에 등록된다.
# 서비스와 연결된 Endpoints 확인
kubectl get endpoints sleep-23단계: ClusterIP 할당
서비스에 고정된 가상 IP(ClusterIP)가 자동으로 할당된다. 이 IP는 서비스가 삭제될 때까지 변하지 않는다.
# 서비스 정보 확인 — CLUSTER-IP 컬럼
kubectl get svc sleep-24단계: kube-proxy가 라우팅 규칙 설정
각 노드의 kube-proxy가 iptables 규칙을 설정한다. 파드에서 ClusterIP:Port로 보낸 트래픽은 selector와 매칭되는 파드들로 로드밸런싱되어 전달된다.
실제 트래픽 흐름
모든 단계를 종합하면 실제 트래픽은 이렇게 흐른다.
- 파드A에서
http://sleep-2:80으로 요청 - 클러스터 내부 DNS가
sleep-2를 ClusterIP로 변환 - kube-proxy가 iptables 규칙을 적용해 실제 Pod IP/Port로 라운드로빈 등 방식으로 전달
- 파드B가 실제 요청을 처리
핵심
결국 서비스는 파드 집합 앞에 놓이는 고정된 문이다. 파드가 교체되어도 서비스의 IP와 이름은 그대로 유지되고, 뒤에서 Endpoints와 kube-proxy가 자동으로 새 파드로 연결을 갱신한다.
4. 파드 간 통신: ClusterIP
ClusterIP는 가장 기본이 되는 서비스 타입이다. 클러스터 내부 통신 전용이다.
특성
- 클러스터 전체에서 통용되는 가상 IP 주소를 생성한다
- 파드가 어느 노드에 있더라도 접근 가능하다
- 클러스터 외부에서는 접근할 수 없다 — 내부 전용
- 파드가 재시작해도 selector로 자동 인식되어 통신이 유지된다
YAML 정의와 적용
apiVersion: v1
kind: Service
metadata:
name: sleep-2
spec:
selector:
app: sleep-2 # 이 레이블을 가진 파드에 트래픽 전달
ports:
- port: 80 # 서비스가 주시하는 포트
type: ClusterIP # 기본값이나 명시하는 게 가독성에 좋다# 서비스 적용
kubectl apply -f service.yaml
# 서비스 확인
kubectl get svc sleep-2
# 서비스와 연결된 파드 확인
kubectl get endpoints sleep-2- 서비스 이름(
sleep-2)이 곧 클러스터 내부 도메인 네임이다 app: sleep-2레이블을 가진 모든 파드가 트래픽 대상이다- 파드의 80번 포트로 트래픽을 전달한다
ping은 안 된다
서비스에 ping을 보내면 실패한다. ping은 ICMP 프로토콜을 사용하는데, k8s Service는 TCP/UDP만 지원하기 때문이다.
5. 외부 트래픽을 파드로 전달하기
ClusterIP는 클러스터 내부 전용이다. 외부 사용자가 애플리케이션에 접근하려면 어떻게 해야 할까?
이전 글에서 kubectl port-forward를 사용했지만, 이건 개발 중 임시 확인용이지 실제 서비스에 쓸 수 있는 방법이 아니다. 외부 트래픽을 파드로 전달하는 서비스 타입이 따로 있다.
LoadBalancer 서비스
LoadBalancer는 외부 로드밸런서와 함께 동작하는 서비스 타입이다. 커버 범위는 클러스터 전체이므로, 트래픽을 받는 노드가 아닌 다른 노드의 파드에도 트래픽을 전달할 수 있다.
apiVersion: v1
kind: Service
metadata:
name: numbers-web
spec:
ports:
- port: 8080 # 서비스가 주시하는 포트
targetPort: 80 # 트래픽이 전달될 파드의 포트
selector:
app: numbers-web
type: LoadBalancer# 서비스 적용
kubectl apply -f lb-service.yaml
# EXTERNAL-IP 확인
kubectl get svc numbers-web이 서비스를 적용하면 kubectl port-forward 없이 localhost:8080으로 웹 애플리케이션에 접근할 수 있다.
환경별 EXTERNAL-IP 차이
kubectl get svc의 EXTERNAL-IP는 실행 환경에 따라 다르게 나타난다.
- Docker Desktop:
localhost로 보인다. 로컬 컴퓨터의 네트워크 스택과 통합되어 있어서 로드밸런서 서비스가 로컬 호스트 주소를 사용한다. 로드밸런서 서비스 여러 개를 쓰려면 포트를 각각 다르게 해야 한다 - k3d: 내부 사설 IP가 보인다. 실제 접근은 호스트에 publish한 포트를 통해
localhost:<port>로 한다. 여러 개를 쓰려면 역시 포트를 다르게 설정해야 한다 - EKS: 클라우드에서 제공하는 공인 IP가 된다. 실제로 AWS에 로드밸런서가 만들어지며, 각 서비스의 IP 주소도 각기 다르다. 공인 IP로 인터넷에서 접근 가능하다
NodePort 서비스
클러스터를 구성하는 모든 노드가 서비스에 지정된 포트를 주시하며, 들어온 트래픽을 파드의 대상 포트로 전달한다. 로드밸런서 없이 동작한다.
단점이 있어 실무에서는 거의 쓰지 않는다.
- 포트 범위가 30000~32767로 제한된다
- 서비스에서 설정된 포트가 모든 노드에서 개방되어 있어야 한다
- 다중 노드 클러스터에서 로드밸런싱 효과를 얻기 어렵다
LoadBalancer vs NodePort
- LoadBalancer: 외부 로드밸런서가
<ExternalIP>:<ServicePort>로 접근 제공. 클라우드 환경에서 주로 사용. Service에 정의한 포트를 그대로 사용 가능 - NodePort:
<NodeIP>:<NodePort>로 접근. 포트 범위(30000~32767) 제한. 외부 IP 직접 제공하지 않음. 실무에서는 잘 안 쓴다
6. 클러스터에서 외부로 트래픽 전달하기
지금까지는 외부에서 클러스터 안으로 들어오는 트래픽을 다뤘다. 반대로, 클러스터 내부의 파드가 외부 서비스(DB, API 등)에 접근해야 할 때는 어떻게 할까?
ExternalName 서비스
ExternalName 서비스는 클러스터 외부의 DNS 이름으로 트래픽을 치환만 해주는 서비스다. k8s는 CNAME(Canonical Name)을 사용해서 구현한다. CNAME은 DNS에서 사용하는 alias다.
apiVersion: v1
kind: Service
metadata:
name: numbers-api
spec:
type: ExternalName
externalName: raw.githubusercontent.com# 적용 후 확인 — EXTERNAL-IP에 외부 도메인이 보인다
kubectl apply -f externalname.yaml
kubectl get svc numbers-api파드 안에서 numbers-api를 조회하면 DNS가 CNAME인 raw.githubusercontent.com을 응답한다.
왜 쓰는가
애플리케이션 코드에서 DB 서버 주소를 하드코딩하는 대신 서비스 이름(numbers-api)을 사용한다. 환경에 따라 ExternalName만 바꾸면 된다.
- 개발 환경: ExternalName이 클러스터 내부의 테스트 DB를 가리킴
- 운영 환경: ExternalName이 실제 운영 DB를 가리킴
코드 변경 없이 환경 간 차이를 반영할 수 있다.
ExternalName의 HTTP Host 헤더 문제
ExternalName은 DNS 수준에서 주소만 치환할 뿐, 요청 내용 자체를 바꾸지 못한다. 이것이 HTTP에서 문제가 된다.
- TCP는 IP + port로만 연결하므로 Host 개념이 없다 — 문제 없음
- HTTP/1.1 요청에는 Host 헤더가 들어간다 — 문제 발생
구체적인 시나리오:
- 파드에서
http://numbers-api/data를 요청하면 Host 헤더에numbers-api.default.svc.cluster.local이 들어간다 - DNS가 CNAME인
raw.githubusercontent.com으로 연결해준다 - 하지만 GitHub 서버는 Host 헤더가
raw.githubusercontent.com이 아니라numbers-api.default.svc.cluster.local인 것을 보고 요청을 거부한다
헤드리스 서비스와 Endpoints 직접 정의
ExternalName의 HTTP Host 헤더 문제를 우회하는 방법이다. 핵심 아이디어는 selector가 없는 ClusterIP 서비스를 만들고, Endpoints 리소스에 외부 IP를 직접 지정하는 것이다.
한 YAML 파일에 두 개의 리소스를 정의한다.
kind: Service+type: ClusterIP: selector 필드가 없는 서비스kind: Endpoints: 외부 서비스의 정적 IP 주소 목록과 port 정보
이 방식은 DNS 치환이 아니라 ClusterIP를 통해 직접 IP로 라우팅하므로 Host 헤더 문제가 발생하지 않는다.
주의
Endpoints에 정의된 IP 주소가 실재하지 않더라도 k8s는 확인하지 않는다. 잘못된 IP를 넣으면 조용히 연결 실패한다.
7. Endpoints와 EndpointSlice
서비스가 트래픽을 전달할 “실제 목적지 목록”을 관리하는 리소스가 Endpoints와 EndpointSlice다. 보통은 k8s가 자동으로 관리하지만, 서비스 트러블슈팅에서 자주 확인하게 된다.
리소스 간 관계
Deployment → Pod 생성 (Pod에 IP 할당)
Service → selector로 Pod 선택
Endpoints/EndpointSlice → 선택된 Pod의 IP:Port를 기록
서비스에 selector가 있고, 해당 selector와 일치하는 Ready 상태의 파드가 있으면 k8s가 Endpoints를 자동으로 생성하고 관리한다. 직접 만들 필요가 없다.
Endpoints
Service가 연결할 backend Pod의 IP와 port 목록이다.
# 전체 Endpoints 확인
kubectl get endpoints
kubectl get ep
# 특정 Service의 Endpoints 확인
kubectl get endpoints numbers-api
kubectl describe endpoints numbers-api예시 출력:
NAME ENDPOINTS AGE
numbers-api 10.42.1.20:80 10m- ENDPOINTS가 비어 있다면 Service selector와 일치하는 Ready Pod가 없다는 뜻이다
- Endpoints는 Deployment를 직접 가리키는 것이 아니라, Service selector에 매칭된 Pod IP를 기록한다
EndpointSlice
EndpointSlice는 Endpoints의 확장성 문제를 해결하기 위해 등장했다. Pod가 수백 개인 Service에서 Endpoints 객체 하나가 너무 커지면 API 서버와 kube-proxy에 부담이 된다. EndpointSlice는 목록을 여러 조각으로 나눠 관리한다.
# EndpointSlice 확인
kubectl get endpointslices
# 특정 Service와 연결된 EndpointSlice 검색
kubectl get endpointslices -l kubernetes.io/service-name=numbers-api
# 상세 조회
kubectl describe endpointslice <endpointslice-name>EndpointSlice 이름은 보통 Service 이름 뒤에 suffix가 붙는다. (예: numbers-api-abcde)
Endpoints vs EndpointSlice
Endpoints는 구형/호환성 중심이고, EndpointSlice는 최신 k8s에서 내부적으로 더 중심이 되는 객체다. 실무적으로 간단 확인은
kubectl get endpoints, 자세한 상태 확인은kubectl get endpointslices를 같이 보면 된다.
Endpoint 트러블슈팅
서비스에 트래픽이 안 가면 Endpoints부터 확인한다.
# 1. Service의 selector 확인
kubectl describe svc <service-name>
# 2. Pod에 해당 label이 있는지 확인
kubectl get pods --show-labels
# 3. Endpoints에 Pod IP가 잡혀 있는지 확인
kubectl get endpoints <service-name>
# 4. EndpointSlice도 확인
kubectl get endpointslices -l kubernetes.io/service-name=<service-name>
# 전체를 한눈에 보려면
kubectl get svc,endpoints,endpointslices8. k8s 서비스의 DNS 해소 과정
지금까지 서비스의 종류와 Endpoints를 다뤘다. 마지막으로, 서비스의 DNS 해소가 내부적으로 어떻게 동작하는지 정리한다.
DNS가 ClusterIP를 반환하는 이유
파드가 서비스 이름으로 DNS를 조회하면, k8s DNS 서버는 엔드포인트의 Pod IP가 아니라 ClusterIP를 반환한다. 왜 그럴까?
- 엔드포인트가 가리키는 Pod IP는 파드 교체 시 계속 변한다
- ClusterIP는 서비스가 삭제되지 않는 한 절대 변하지 않는다
- 덕분에 클라이언트(파드)가 DNS 조회 결과를 영구적으로 캐시할 수 있다
실제 라우팅은 각 노드의 kube-proxy가 담당한다. kube-proxy는 모든 서비스의 엔드포인트에 대한 최신 정보를 유지하며, OS의 [방화벽과 원격 접속|패킷 필터링] 기능을 사용해 ClusterIP로 향하는 트래픽을 실제 Pod IP로 변환한다.
요약하면:
- 파드가 서비스 이름으로 DNS 조회 → ClusterIP 반환 (안정적, 캐시 가능)
- 파드가 ClusterIP로 트래픽 전송
- 해당 노드의 kube-proxy가 iptables 규칙으로 ClusterIP → 실제 Pod IP 변환
- 실제 Pod가 요청 처리
도메인 네임은 왜 .default.svc.cluster.local로 끝날까
모든 k8s 리소스는 네임스페이스 안에 존재한다. 네임스페이스는 클러스터를 논리적 파티션으로 나누는 역할을 한다.
서비스의 전체 도메인 네임은 <서비스이름>.<네임스페이스>.svc.cluster.local 형식이다.
sleep-2.default.svc.cluster.local에서:sleep-2: 서비스 이름default: 네임스페이스 이름 (기본값)svc.cluster.local: k8s 클러스터의 서비스 도메인 접미사
클러스터에는 여러 네임스페이스가 존재한다.
- default: 기본 네임스페이스. YAML에서 따로 지정하지 않으면 여기에 생성된다
- kube-system: DNS 서버, k8s API 등 내장 컴포넌트가 동작하는 네임스페이스
같은 네임스페이스 안에서는 서비스 이름만으로 접근할 수 있고, 다른 네임스페이스의 서비스에 접근하려면 <서비스이름>.<네임스페이스> 형식을 사용한다.
# 같은 네임스페이스 — 이름만으로 충분
curl http://sleep-2:80
# 다른 네임스페이스의 서비스 접근
curl http://sleep-2.other-namespace:80기본 kubernetes 서비스
기본 kubernetes 서비스의 정체
kubectl get svc를 실행하면 항상kubernetes라는 ClusterIP 서비스가 존재한다- 이 서비스의 역할은 클러스터 내부에서 k8s API 서버(control-plane)로 트래픽을 전달하는 것이다
- 일반 서비스와 달리 selector가 없고, Endpoints 리소스로 API 서버의 실제 IP/Port와 직접 연결되어 있다
- 이 서비스를 삭제해도 control-plane이 자동으로 재생성한다 — k8s 핵심 기능도 k8s 애플리케이션 형태로 동작하기 때문이다
# 기본 서비스 확인
kubectl get svc kubernetes
# API 서버의 실제 IP/Port 확인
kubectl get endpoints kubernetes