Certified Kubernetes Application Developer (CKAD) #18 Services: ClusterIP, NodePort, LoadBalancer, ExternalName
#17 Volumes에서 Pod에 데이터를 붙였다면, 이번에는 그 Pod에 안정적으로 접속하는 방법을 다룹니다. Deployment가 Pod를 굴리는 동안 Pod는 끊임없이 생기고 사라집니다. 롤링 업데이트가 일어나면 IP가 통째로 바뀌고, 노드가 죽으면 다른 노드에서 새 Pod가 뜹니다. 이렇게 바뀌는 Pod 집합 앞에 변하지 않는 진입점을 세우는 객체가 Service입니다.
Service는 selector로 Pod를 고르고, 고른 Pod의 IP 목록을 자동으로 추적하면서, 클라이언트에게는 고정된 이름과 가상 IP 하나만 노출합니다. 이번 글에서는 Service의 동작 원리와 네 가지 타입, 포트 세 종류의 구분, DNS 규칙, 그리고 시험에서 자주 나오는 엔드포인트 디버깅까지 정리하겠습니다.
왜 Service가 필요한가 #
Pod의 IP는 일회용입니다. Pod가 재시작되거나 다른 노드로 옮겨지면 IP가 바뀌고, Deployment의 replicas가 여러 개면 클라이언트는 어느 IP로 보내야 할지조차 알 수 없습니다. 그래서 Pod IP를 직접 호출하는 코드는 곧바로 깨집니다.
Service는 이 문제를 두 가지로 풉니다.
- 고정된 진입점. Service에는 변하지 않는 이름(DNS)과 ClusterIP가 부여됩니다. 뒤의 Pod가 아무리 바뀌어도 클라이언트는 같은 주소로 보냅니다.
- 부하 분산. selector에 맞는 여러 Pod로 요청을 자동으로 분산합니다.
쿠버네티스의 기초 개념을 더 넓게 잡고 싶다면 K8s 실무 트랙 #5 Service와 네트워킹을 함께 보면 좋습니다. 이 글은 CKAD 실기 관점에서 명령과 매니페스트에 집중하겠습니다.
selector와 label, 그리고 Endpoints #
Service는 Pod를 직접 지정하지 않습니다. 대신 spec.selector에 label 조건을 적어 두면, 그 조건에 맞는 Pod를 클러스터가 알아서 찾아 줍니다. 이 연결을 실제로 들고 있는 객체가 Endpoints(또는 EndpointSlice)입니다.
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web # 이 label을 가진 Pod를 모두 선택
ports:
- port: 80
targetPort: 8080위 Service는 app=web label을 가진 Pod를 찾고, 그 Pod들의 IP와 포트를 Endpoints에 자동으로 채웁니다. Pod가 새로 뜨면 Endpoints에 추가되고, 사라지면 빠집니다. 이 자동 관리가 Service의 핵심입니다.
# Service가 실제로 가리키는 Pod 목록 확인
k get endpoints web
k get endpointslices -l kubernetes.io/service-name=webselector가 Pod의 label과 한 글자라도 어긋나면 Endpoints가 비고, 그러면 Service는 연결을 받아도 보낼 곳이 없습니다. 이 사례는 뒤의 디버깅 절에서 다시 다루겠습니다.
Service 타입 네 가지 #
Service는 외부에 어디까지 노출하느냐에 따라 네 타입으로 나뉩니다. 타입을 지정하지 않으면 기본값은 ClusterIP입니다.
| 타입 | 접근 범위 | 동작 | 주 쓰임 |
|---|---|---|---|
| ClusterIP | 클러스터 내부 | 가상 IP 하나를 부여, 내부에서만 접근 | 내부 마이크로서비스 간 통신 |
| NodePort | 노드 IP:포트 | 모든 노드의 같은 포트(30000〜32767)를 연다 | 외부 노출, LB 없는 환경 테스트 |
| LoadBalancer | 외부 LB IP | 클라우드 LB를 프로비저닝, NodePort를 감싼다 | 클라우드에서 외부 서비스 노출 |
| ExternalName | DNS CNAME | selector 없이 외부 도메인으로 CNAME만 반환 | 클러스터 밖 서비스에 별칭 부여 |
이 네 타입은 포함 관계로 이해하면 쉽습니다. LoadBalancer는 NodePort를 포함하고, NodePort는 ClusterIP를 포함합니다. 즉 NodePort를 만들면 ClusterIP도 같이 생기고, LoadBalancer를 만들면 NodePort와 ClusterIP가 모두 함께 생깁니다. ExternalName만 selector도 ClusterIP도 없는 별종으로, 단순히 DNS 응답을 CNAME으로 돌려주는 객체입니다.
ClusterIP #
가장 기본이 되는 타입입니다. 클러스터 내부 전용 가상 IP를 받고, 외부에서는 접근할 수 없습니다. 마이크로서비스끼리 서로를 호출할 때 거의 항상 이 타입을 씁니다.
NodePort #
모든 노드에서 같은 포트를 열어, 노드의 IP와 그 포트로 들어온 트래픽을 Service로 넘깁니다. 포트 범위는 30000〜32767이며, 지정하지 않으면 이 범위에서 자동으로 하나가 할당됩니다. LB가 없는 환경에서 외부로 빠르게 노출하거나 테스트할 때 유용합니다.
LoadBalancer #
클라우드 제공자(AWS, GCP, Azure 등)의 외부 로드밸런서를 프로비저닝합니다. 외부 IP가 부여되고, 그 IP로 들어온 트래픽이 NodePort를 거쳐 Pod로 전달됩니다. 클라우드가 아닌 환경에서는 외부 IP가 <pending>에 머무를 수 있습니다.
ExternalName #
selector도 ClusterIP도 갖지 않습니다. 대신 spec.externalName에 적은 외부 도메인으로 향하는 CNAME 레코드를 반환합니다. 클러스터 안의 코드가 외부 데이터베이스 같은 자원을 내부 이름으로 부르게 만들 때 씁니다.
apiVersion: v1
kind: Service
metadata:
name: external-db
spec:
type: ExternalName
externalName: db.example.com # 이 이름으로 CNAME만 반환port vs targetPort vs nodePort #
CKAD에서 가장 헷갈리는 지점이 포트 세 종류입니다. 각각이 가리키는 대상이 다릅니다.
| 필드 | 어디의 포트인가 | 설명 |
|---|---|---|
| port | Service 자신 | 클라이언트가 Service로 접속할 때 쓰는 포트 |
| targetPort | Pod 컨테이너 | Service가 트래픽을 넘길 컨테이너 포트 |
| nodePort | 노드 | NodePort/LoadBalancer에서 노드가 외부로 여는 포트(30000〜32767) |
흐름은 이렇습니다. 외부 또는 노드의 nodePort로 들어온 트래픽이 Service의 port를 거쳐, 최종적으로 Pod의 targetPort로 전달됩니다. targetPort를 생략하면 port와 같은 값으로 간주합니다. 컨테이너가 8080에서 듣는데 Service의 port를 80으로 열고 싶다면 targetPort: 8080을 반드시 명시해야 합니다.
명령형으로 Service 만들기 #
시험에서는 매니페스트를 손으로 쓰기보다 generator로 뼈대를 뽑는 편이 빠릅니다. 두 명령을 외워 두면 대부분의 Service 작업을 처리할 수 있습니다.
# 1) 기존 Deployment를 노출 (selector를 Deployment label에서 자동 추출)
k expose deploy web --port=80 --target-port=8080
# 2) Service를 직접 생성 (selector는 직접 지정 필요)
k create svc clusterip web --tcp=80:8080
k create svc nodeport web --tcp=80:8080 --node-port=30080
# dry-run으로 매니페스트 뼈대만 뽑기
k expose deploy web --port=80 --target-port=8080 $do > svc.yamlk expose는 대상 워크로드의 label을 그대로 selector로 가져오므로 가장 손이 적게 듭니다. 다만 타입을 바꾸려면 --type=NodePort처럼 옵션을 더하거나, 만든 뒤 k edit로 고칩니다.
# 노출하면서 타입까지 지정
k expose deploy web --port=80 --target-port=8080 --type=NodePortYAML 예제 #
ClusterIP #
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: ClusterIP # 생략 시 기본값
selector:
app: web
ports:
- protocol: TCP
port: 80 # Service 포트
targetPort: 8080 # 컨테이너 포트클러스터 내부의 다른 Pod는 web 또는 web.<namespace>이름으로 이 Service에 접속합니다.
NodePort #
apiVersion: v1
kind: Service
metadata:
name: web-np
spec:
type: NodePort
selector:
app: web
ports:
- protocol: TCP
port: 80 # 클러스터 내부에서 쓰는 Service 포트
targetPort: 8080 # 컨테이너 포트
nodePort: 30080 # 노드가 외부로 여는 포트(30000〜32767, 생략 시 자동)이 Service는 모든 노드의 30080 포트로 들어온 트래픽을 app=web Pod의 8080으로 넘깁니다. 외부에서는 <노드IP>:30080으로 접근합니다.
headless Service #
clusterIP: None으로 지정하면 가상 IP를 받지 않는 headless Service가 됩니다. 이 경우 Service 이름을 DNS로 조회하면 단일 ClusterIP가 아니라 각 Pod의 IP 목록이 그대로 반환됩니다. StatefulSet과 함께 쓰여, 각 Pod에 <pod>.<service>.<namespace>.svc.cluster.local 형태의 고정 DNS 이름을 부여하는 데 자주 쓰입니다.
apiVersion: v1
kind: Service
metadata:
name: db
spec:
clusterIP: None # headless
selector:
app: db
ports:
- port: 5432클러스터 DNS #
Service를 만들면 CoreDNS가 자동으로 DNS 레코드를 등록합니다. 정식 이름(FQDN)은 다음 형식입니다.
<service>.<namespace>.svc.cluster.local예를 들어 prod 네임스페이스의 web Service는 web.prod.svc.cluster.local로 조회됩니다. 같은 네임스페이스 안의 Pod는 짧은 이름인 web만으로 접근할 수 있고, 다른 네임스페이스의 Service를 부를 때는 최소한 web.prod처럼 네임스페이스를 붙입니다.
# 임시 Pod에서 DNS 확인
k run tmp --image=busybox --rm -it --restart=Never -- nslookup web.prod.svc.cluster.local
# 같은 네임스페이스라면 짧은 이름으로
k run tmp --image=busybox --rm -it --restart=Never -- wget -qO- web디버깅: 엔드포인트가 비는 경우 #
시험에서 “Service로 접속이 안 된다” 유형이 나오면 가장 먼저 Endpoints를 확인합니다. Endpoints가 비어 있으면 Service가 가리킬 Pod를 못 찾았다는 뜻이고, 거의 항상 selector와 Pod label 불일치 때문입니다.
# 1) Endpoints에 Pod IP가 채워졌는지
k get endpoints web
# 2) Service의 selector 확인
k describe svc web | grep -i selector
# 3) 실제 Pod의 label 확인
k get pods --show-labels예를 들어 Service의 selector가 app: web인데 Pod의 label이 app: webapp이라면, 두 값이 다르므로 Endpoints가 비고 접속도 실패합니다. 이때는 Service의 selector를 고치거나 Pod의 label을 맞춥니다.
# Pod label을 Service selector에 맞추기
k label pod web-xxxxx app=web --overwrite엔드포인트가 비는 또 다른 원인은 targetPort가 컨테이너의 실제 listen 포트와 다른 경우, 또는 readiness probe가 실패해 Pod가 NotReady라 Endpoints에서 제외되는 경우입니다. Endpoints가 비었는지, 채워졌는데 포트가 틀렸는지부터 가르면 원인을 빠르게 좁힐 수 있습니다.
# Service까지 도달하는지 임시 Pod로 확인
k run probe --image=busybox --rm -it --restart=Never -- wget -qO- web:80시험 포인트 #
- 타입 기본값은 ClusterIP. NodePort,LoadBalancer는 ClusterIP를 포함하고, ExternalName만 selector와 ClusterIP가 없는 CNAME 전용입니다.
- 포트 세 종류를 정확히 구분.
port는 Service,targetPort는 컨테이너,nodePort는 노드입니다. nodePort 범위는 30000〜32767입니다. - **
k expose deploy ... --port= --target-port=**가 가장 빠른 노출 명령입니다. selector를 자동으로 가져옵니다. - headless는
clusterIP: None. StatefulSet과 함께 Pod 별 DNS를 만듭니다. - DNS는
<service>.<namespace>.svc.cluster.local. 같은 네임스페이스는 짧은 이름으로 충분합니다. - 접속 실패는
k get endpoints부터. 비어 있으면 selector와 label 불일치를 의심합니다.
정리 #
이번 글에서 잡은 것:
- Service는 바뀌는 Pod 집합 앞의 고정된 진입점이며, selector로 Pod를 골라 Endpoints를 자동 관리합니다.
- 타입 네 가지(ClusterIP,NodePort,LoadBalancer,ExternalName)는 노출 범위가 다르고, 앞의 셋은 포함 관계입니다.
- 포트는
port(Service),targetPort(컨테이너),nodePort(노드) 세 종류로 나뉩니다. - headless Service와 클러스터 DNS 규칙, 그리고 엔드포인트가 비는 디버깅까지 명령으로 익혔습니다.
다음: Ingress와 NetworkPolicy #
Service로 진입점은 세웠지만, 외부 HTTP 트래픽을 경로별로 라우팅하거나 Pod 사이 통신을 막고 여는 일은 Service만으로는 부족합니다.
#19 Ingress와 NetworkPolicy에서는 여러 Service를 호스트,경로 규칙으로 묶는 Ingress, TLS 종료, 그리고 default-deny에서 시작해 ingress,egress 규칙으로 통신을 제어하는 NetworkPolicy를 YAML과 kubectl로 다루겠습니다.