SUMMARY
서비스(Service)는 레이블 셀렉터(Label Selector)로 특정 파드(Pod)들을 골라내고, 그 파드들의 IP·포트를 EndpointSlice에 모아 관리한다. 서비스 IP로 요청이 들어오면 각 노드의 kube-proxy가 이 목록을 참조해서 실제 파드로 트래픽을 전달한다.
1. 서비스가 필요한 이유
파드는 일시적인 자원이다. 장애, 스케일링, 업데이트 같은 이유로 언제든 재생성되거나 삭제되며, 그때마다 새로운 IP가 할당된다. 그래서 파드 IP를 직접 참조하면 파드가 교체될 때마다 연결이 끊긴다.
서비스(Service)는 파드 집합에 고정된 IP(ClusterIP)와 DNS 이름을 부여하는 추상화다. 파드가 교체되더라도 클라이언트는 항상 같은 주소로 접근할 수 있다.
2. 서비스가 파드를 찾는 방식
서비스가 자신의 백엔드가 될 파드를 어떻게 선택하는지가 핵심이다. 그 중심에 레이블 셀렉터(Label Selector)가 있다.
2-1. 레이블 셀렉터 매칭
서비스는 selector에 적힌 레이블을 기준으로, 같은 레이블을 가진 파드를 백엔드로 사용한다.
아래는 app: web, tier: frontend 레이블을 가진 파드를 고르는 서비스다.
# 'app: web', 'tier: frontend' 레이블을 가진 파드를 고른다
apiVersion: v1
kind: Service
metadata:
name: web-service
spec:
selector:
app: web
tier: frontend
ports:
- protocol: TCP
port: 80 # 서비스가 노출할 포트
targetPort: 8080 # 트래픽을 전달할 파드의 컨테이너 포트이 서비스에 연결되려면 파드는 metadata.labels에 해당 레이블을 모두 가지고 있어야 한다.
# 위 서비스의 selector와 일치하는 파드
apiVersion: v1
kind: Pod
metadata:
name: web-pod
labels:
app: web
tier: frontend
spec:
containers:
- name: web-container
image: nginx
ports:
- containerPort: 80802-2. EndpointSlice로 이어지는 과정
서비스와 파드를 연결하는 실제 작업은 컨트롤러가 자동으로 수행한다.
- 서비스 매니페스트를 API 서버에 제출한다.
- 컨트롤러가 서비스의
selector와 일치하는 파드를 클러스터에서 찾는다. - 찾은 파드들의 IP와 포트를 EndpointSlice 오브젝트에 기록한다.
- 이후에도 파드가 생성되거나 종료되고 레이블이 바뀔 때마다 EndpointSlice를 최신 상태로 갱신한다.
# web-service에 연결된 파드들의 IP·포트를 담는다
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: web-service-abcde
labels:
kubernetes.io/service-name: web-service # 어떤 서비스에 속하는지 표시
addressType: IPv4
ports:
- protocol: TCP
port: 8080
endpoints:
- addresses:
- "10.244.1.5"
- addresses:
- "10.244.2.3"NOTE
예전에는 파드 IP를 하나의
Endpoints오브젝트에 모아 담았는데, 지금은 이 방식이 deprecated 되었고 여러 개로 나누어 담는 EndpointSlice가 표준이다. 서비스 수가 늘어나고 엔드포인트가 증가할 때 부담을 줄이려는 구조다. 오래된 클러스터나 문서에서는 아직Endpoints라는 이름을 볼 수 있다.
3. kube-proxy와 트래픽 라우팅
서비스 개념을 실제로 구현하는 것은 kube-proxy다. kube-proxy는 클러스터의 모든 노드에서 실행되면서 서비스 IP와 파드 IP를 매핑하고, 라우팅 규칙을 관리하며, 파드로 향하는 트래픽을 로드 밸런싱한다. 서비스 IP(ClusterIP)는 실제 인터페이스가 없는 가상 IP이므로, 이 규칙이 없으면 어디에도 도달하지 못한다.
graph LR A[클라이언트 파드] --> B{web-service ClusterIP}; subgraph "Node" B --> C[kube-proxy 규칙]; C -- iptables/IPVS --> D[web-pod-1 10.244.1.5]; C -- iptables/IPVS --> E[web-pod-2 10.244.2.3]; end
- 클라이언트가 서비스의 ClusterIP로 요청을 보낸다.
- kube-proxy는 EndpointSlice로부터 실제 파드 IP 목록을 이미 알고 있다.
- 이 목록을 바탕으로
iptables·IPVS(최신 버전은nftables도 지원) 규칙을 설정해서 요청을 백엔드 파드 중 하나로 전달한다. 이때 로드 밸런싱이 이루어진다. iptables 모드는 순번대로가 아니라 확률적으로(무작위) 선택하고, IPVS는 기본적으로 라운드 로빈 방식을 사용한다.
INFO
kube-proxy는 이름과 달리 트래픽을 직접 받아서 전달하는 프록시 서버가 아니다. 커널의 넷필터(Netfilter)를 이용해 패킷의 목적지 주소를 변경하는(DNAT) 규칙을 미리 설정해 두는 쪽에 가깝다.
4. kube-proxy 없이 동작시키기
kube-proxy 대신 CNI가 서비스 라우팅을 담당할 수도 있다. Cilium을 사용하면 agent 내부의 eBPF 데이터플레인으로 kube-proxy 기능을 대체한다.
- kube-proxy 컴포넌트를 아예 안 깔아도 된다.
- iptables·IPVS 규칙에 대한 의존성을 제거할 수 있다. iptables 정책이 꼬여서 서비스 통신이 깨지는 상황 자체를 피할 수 있다.
- eBPF는 커널 레벨 패킷 처리 방식이므로 iptables·IPVS보다 빠르고, 서비스와 엔드포인트가 많아져도 성능 저하가 덜하다.
- 로드 밸런싱 옵션, 클라이언트 IP 보존, DSR 같은 기능도 지원한다.
5. 연결이 안 될 때 점검
서비스가 연결되지 않을 때 가장 흔한 원인은 레이블 불일치다. selector와 파드 labels가 정확히 일치하는지부터 확인한다.
# 1. 서비스의 selector와 할당된 IP 확인
kubectl describe svc web-service
# 2. 서비스에 연결된 엔드포인트 확인. 비어 있으면 매칭되는 파드가 없다는 뜻이다.
kubectl get endpointslices -l kubernetes.io/service-name=web-service
# 3. 파드 레이블 직접 확인
kubectl get pods --show-labels점검 시에는 주로 아래 세 가지를 확인한다.
- 서비스
selector와 파드labels가 일치하는가? - 서비스
targetPort가 파드의containerPort와 맞는가? - 파드가
Running,Ready인가?
6. 서비스 타입
노출 방식에 따라 서비스 타입이 나뉜다.
- ClusterIP: 기본값. 클러스터 내부에서만 접근 가능한 가상 IP를 부여한다.
- NodePort: ClusterIP에 더해서, 모든 노드의 특정 포트(기본 범위 30000-32767)로 외부 접근을 허용한다.
<NodeIP>:<NodePort>형태로 연결한다. - LoadBalancer: NodePort에 더해서, 클라우드의 외부 로드 밸런서를 프로비저닝하여 연결한다. 외부 IP가 생성되므로 클러스터 밖에서 바로 접근한다.
- ExternalName: 클러스터 밖 서비스를 DNS CNAME으로 매핑해서, 내부 서비스처럼 별칭으로 사용한다.