Table of contents
- 파드 IP를 믿으면 안 되는 이유
- Service가 세우는 구조
- Endpoints: Service 뒤에서 일어나는 진짜 일
- Service 타입 네 가지
- DNS: Service 이름이 그대로 주소가 되는 이유
- 파드에서 Service를 부르는 실전
- Headless Service: 파드 IP를 직접 알고 싶을 때
- 네트워크 정책: 파드 간 통신 제어
- 한 장으로 정리
파드 IP를 믿으면 안 되는 이유
3편에서 파드는 일회용이라고 못 박았다. 죽으면 IP가 사라지고 다시 뜨면 다른 IP를 받는다. 이 점이 Service라는 추상화가 왜 필요한지 설명해준다.
만약 프론트엔드가 백엔드 파드를 IP로 직접 호출한다면 어떻게 될까? 백엔드 파드가 재시작되는 순간 연결이 전부 깨진다. 3개로 스케일 아웃해도 프론트엔드는 그 중 하나의 IP만 알고 있으니 부하 분산도 안 된다. Kubernetes가 아무리 파드를 잘 관리해도, 접근하는 쪽에서 파드 IP를 직접 쓰면 이 장점이 모두 무너진다.
Service는 여러 파드 앞에 고정된 접근점을 만들어주는 추상화다. 파드 IP는 수시로 바뀌어도, Service의 IP(ClusterIP)는 안정적으로 유지된다. 그래서 호출하는 쪽은 파드 개수, 파드 위치, 파드 상태를 신경 쓸 필요 없이 Service 이름만 알면 된다.
Service가 세우는 구조
Service를 이해하기 위해 가장 단순한 예부터 보자. 백엔드 파드 3개 앞에 Service 하나를 세우는 상황이다.
# backend-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
spec:
replicas: 3
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: myapp:1.0
ports:
- containerPort: 8080
---
# backend-service.yaml
apiVersion: v1
kind: Service
metadata:
name: backend
spec:
selector:
app: backend
ports:
- port: 80
targetPort: 8080
type: ClusterIP
핵심은 selector다. 이 Service는 app: backend 라벨을 가진 파드들을 자기 뒤에 두겠다고 선언한다. Kubernetes는 이 라벨과 일치하는 파드들을 자동으로 찾아서 연결해준다.
flowchart LR
FE[Frontend Pod] -->|http://backend:80| SVC[Service: backend<br/>ClusterIP: 10.96.0.42]
SVC --> P1[Pod 1<br/>10.244.1.5:8080]
SVC --> P2[Pod 2<br/>10.244.1.6:8080]
SVC --> P3[Pod 3<br/>10.244.2.3:8080]
프론트엔드는 http://backend:80으로 요청을 보낸다. Service의 ClusterIP는 클러스터 안에서만 접근 가능한 가상 IP로, kube-proxy가 관리하는 iptables 규칙을 통해 실제 파드 중 하나에게 전달된다. 파드가 죽고 새로 뜨더라도 Service는 그대로 유지된다.
Endpoints: Service 뒤에서 일어나는 진짜 일
Service를 만들면 같은 이름의 Endpoints 리소스가 자동으로 생긴다. Endpoints는 “이 Service가 현재 어떤 파드 IP들을 가리키는지”의 실시간 리스트다.
kubectl get endpoints backend
# NAME ENDPOINTS AGE
# backend 10.244.1.5:8080,10.244.1.6:8080,10.244.2.3:8080 5m
이 리스트는 고정이 아니다. Endpoints 컨트롤러가 파드의 라벨 변화, readiness 상태, 삭제 이벤트를 감시하면서 계속 업데이트한다. readiness probe를 통과하지 못한 파드는 Endpoints에서 제외된다. 3편에서 readiness가 중요하다고 말한 이유가 여기서 드러난다. readiness가 false면 Service 뒤에서 자동으로 빠지기 때문에, 롤링 업데이트 중에도 트래픽이 준비된 파드에만 간다.
클러스터 규모가 커지면 Endpoints 객체가 너무 비대해져서, EndpointSlice라는 효율적인 형식으로 쪼개서 쓴다. 기능적으로는 같은 일을 한다.
Service 타입 네 가지
Service에는 몇 가지 타입이 있다. 각자 푸는 문제가 다르다. 각 타입이 트래픽 경로를 어떻게 만드는지 한 장에 비교해보자.
flowchart TB
subgraph CIP["ClusterIP (내부 전용)"]
direction TB
CIP_IN["클러스터 내부 Pod"]
CIP_POD["Backend Pods"]
CIP_IN -->|ClusterIP| CIP_POD
end
subgraph NP["NodePort"]
direction TB
NP_EXT["외부 사용자"]
NP_NODE["Node (kube-proxy)"]
NP_POD["Backend Pods"]
NP_EXT -->|"노드IP:30080"| NP_NODE
NP_NODE --> NP_POD
end
subgraph LB["LoadBalancer"]
direction TB
LB_EXT["외부 사용자"]
LB_CLOUD["Cloud LB (ELB/GCLB)"]
LB_NODE["Node"]
LB_POD["Backend Pods"]
LB_EXT -->|"공인 IP:80"| LB_CLOUD
LB_CLOUD --> LB_NODE
LB_NODE --> LB_POD
end
subgraph EN["ExternalName"]
direction TB
EN_IN["내부 Pod"]
EN_DNS["CoreDNS (CNAME)"]
EN_EXT["db.prod.example.com"]
EN_IN -->|"db.mynamespace.svc"| EN_DNS
EN_DNS --> EN_EXT
end
ClusterIP: 내부 전용
기본값. 클러스터 내부에서만 접근할 수 있는 가상 IP를 할당한다. 백엔드, DB, 내부 API 같은 내부 서비스에 쓴다. 외부에 직접 노출되지 않으니 안전하다.
spec:
type: ClusterIP # 생략해도 기본값
NodePort: 노드 포트로 개방
모든 노드의 같은 포트(기본 30000~32767 범위)를 열어서, 그 포트로 들어온 트래픽을 Service에 전달한다.
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
nodePort: 30080
어느 노드 IP로 들어와도 이 Service에 도달한다. 실습이나 로컬 개발에서는 편하지만, 운영에서 NodePort만 쓰는 일은 드물다. 포트 번호가 제한적이고, 노드 IP를 클라이언트가 알아야 하기 때문이다.
LoadBalancer: 클라우드 LB 자동 프로비저닝
AWS, GCP, Azure 같은 클라우드에서 쓰는 타입. Service를 이 타입으로 만들면 Cloud Controller Manager가 해당 클라우드의 로드밸런서(ELB, GCLB 등)를 자동으로 만들어서 앞에 붙인다.
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
kubectl get svc로 조회하면 EXTERNAL-IP 컬럼에 클라우드 LB의 공인 IP가 나온다. 외부에 서비스를 노출할 때 가장 많이 쓰는 방식이다. 단, 오픈프레미스에서는 그대로 동작하지 않고 MetalLB 같은 별도 구성이 필요하다.
ExternalName: 외부 주소에 별칭
클러스터 안에서 “내부 서비스 부르는 것처럼” 외부 주소를 부르고 싶을 때 쓴다. 예를 들어 외부 DB의 DNS에 내부 이름을 붙이고 싶을 때.
spec:
type: ExternalName
externalName: db.prod.example.com
이 Service는 db.mynamespace.svc.cluster.local 같은 이름으로 불러도 db.prod.example.com으로 CNAME 된다. 앱 코드에서 외부 주소를 하드코딩하지 않고 환경별로 추상화할 때 유용하다.
DNS: Service 이름이 그대로 주소가 되는 이유
Kubernetes 클러스터에는 CoreDNS가 기본으로 돌아간다. 이 DNS가 있어서 파드 안에서 curl http://backend 같은 호출이 동작한다.
Service의 DNS 이름 규칙은 이렇다.
<service>.<namespace>.svc.cluster.local
같은 네임스페이스 안에서는 backend만으로 찾을 수 있고, 다른 네임스페이스의 Service는 backend.other-ns처럼 네임스페이스를 붙여야 한다. 실제로 파드 안에 들어가서 /etc/resolv.conf를 열어보면 검색 도메인에 자기 네임스페이스가 들어 있다.
kubectl exec -it some-pod -- cat /etc/resolv.conf
# nameserver 10.96.0.10
# search mynamespace.svc.cluster.local svc.cluster.local cluster.local
# options ndots:5
이 구성 덕분에 네임스페이스가 다르면 코드를 안 바꾸고도 환경별로 다른 서비스를 바라보게 만들 수 있다. dev 네임스페이스에서 backend는 dev의 백엔드, prod 네임스페이스에서 backend는 prod의 백엔드.
파드에서 Service를 부르는 실전
프론트엔드 파드가 백엔드 Service를 부르는 흐름을 정리해보자.
sequenceDiagram
participant APP as Frontend App
participant DNS as CoreDNS
participant IPT as iptables (kube-proxy)
participant POD as Backend Pod
APP->>DNS: backend 조회
DNS-->>APP: 10.96.0.42 (ClusterIP)
APP->>IPT: 10.96.0.42:80 요청
IPT->>POD: 10.244.1.5:8080으로 DNAT
POD-->>APP: 응답
이 전체 과정에서 프론트엔드는 백엔드 파드가 몇 개인지, 어디 있는지, 언제 죽었는지 하나도 몰라도 된다. 그게 Service가 약속하는 추상화다.
Headless Service: 파드 IP를 직접 알고 싶을 때
아주 가끔, 파드 IP 목록 자체가 필요한 경우가 있다. 예를 들어 StatefulSet의 DB 멤버들을 직접 구분해서 접근해야 한다면. 이때 Headless Service를 쓴다.
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
clusterIP: None # 이게 헤드리스
selector:
app: mysql
ports:
- port: 3306
clusterIP: None이 핵심이다. 이 Service는 ClusterIP를 할당받지 않는다. 대신 DNS 조회 결과가 여러 파드 IP를 직접 반환한다. StatefulSet과 함께 쓰면 mysql-0.mysql, mysql-1.mysql 같은 이름으로 개별 파드에 도달할 수 있다. 클러스터 멤버 구성이 필요한 분산 시스템에 주로 쓴다.
네트워크 정책: 파드 간 통신 제어
기본적으로 Kubernetes 클러스터 안의 모든 파드는 서로 통신할 수 있다. 그런데 실무에서는 “프론트엔드 파드는 백엔드 파드만 부를 수 있고, DB는 못 부른다”처럼 제한을 걸고 싶은 경우가 많다.
이걸 선언하는 게 NetworkPolicy다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-allow-frontend
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
이 정책은 “app: backend 파드는 app: frontend 파드로부터 오는 TCP 8080 트래픽만 받는다”를 선언한다. 다른 파드의 요청은 모두 거부된다.
다만 NetworkPolicy는 CNI(Container Network Interface — 컨테이너에 네트워크 인터페이스를 붙이는 표준 스펙) 플러그인이 지원해야 동작한다. Calico, Cilium 같은 CNI는 지원하고, Flannel 같은 단순한 것은 지원하지 않는다. 클러스터의 CNI 선택이 여기서 실질적인 영향을 미친다.
한 장으로 정리
Service 타입과 쓰임새를 표로 정리하면 감이 더 잡힌다.
| 타입 | 범위 | 주요 용도 |
|---|---|---|
| ClusterIP | 클러스터 내부 | 내부 서비스 간 통신 |
| NodePort | 노드 IP:포트 | 단순 외부 노출(개발/실습) |
| LoadBalancer | 클라우드 LB | 외부 트래픽 수용(운영) |
| ExternalName | DNS CNAME | 외부 서비스 별칭 |
| Headless | DNS로 파드 직접 | StatefulSet 멤버 개별 접근 |
이 중에서 서비스를 외부에 노출하는 경로가 LoadBalancer인데, 실무에서는 그 앞에 Ingress라는 한 층을 더 두는 게 일반적이다. 왜 그런지, 어떻게 구성하는지는 다음 편에서 다룬다.
다음 편에서는 외부 트래픽을 받아서 여러 서비스로 분기시키는 Ingress, 그리고 그 뒤를 이어갈 Gateway API를 살펴본다. TLS 종단(termination)과 호스트 기반 라우팅까지 한 번에 정리한다.




Loading comments...