Certified Kubernetes Administrator (CKA) #18 Networking 1: Service (ClusterIP/NodePort/LoadBalancer/ExternalName)

#17 Storage 2로 스토리지 도메인을 끝냈습니다. 이제 CKA 출제 비중 20%를 차지하는 Services and Networking 도메인으로 들어갑니다. 그 첫 글인 이번 편은 네트워킹의 토대인 Service를 다룹니다.

Pod는 언제든 죽고 다시 뜨며, 그때마다 IP가 바뀝니다. 이렇게 흔들리는 Pod IP에 직접 트래픽을 보낼 수는 없습니다. Service는 흔들리는 Pod 집합 앞에 안정된 가상 IP와 DNS 이름을 세워 주는 추상화입니다. 이 시리즈를 K8s 기초 #5에서 개념으로 처음 만났다면, 이번 글은 같은 주제를 CKA 운영자 관점에서 다시 봅니다. Service가 어떻게 selector로 Pod를 고르고, Endpoints를 만들고, kube-proxy가 그것을 노드의 규칙으로 구현하는지, 그리고 Service가 안 될 때 그 경로의 어디가 끊겼는지 추적하는 법을 정리하겠습니다.

Service가 푸는 문제 #

Deployment로 띄운 Pod 3개가 있다고 하겠습니다. 이 3개는 롤링 업데이트, 노드 장애, 스케일 조정 때마다 죽고 다시 뜨며, 그때마다 새 IP를 받습니다. 클라이언트가 특정 PodIP를 외워 두는 방식은 곧바로 깨집니다.

Service는 이 문제를 세 가지로 풉니다.

  • 안정된 가상 IP(ClusterIP). Service 객체가 살아 있는 동안 IP가 바뀌지 않습니다.
  • DNS 이름. CoreDNS가 서비스명.네임스페이스.svc.cluster.local을 ClusterIP로 해석해 줍니다.
  • 부하 분산. selector에 걸린 여러 Pod로 트래픽을 분산합니다.

핵심은 Service가 Pod를 직접 가리키지 않는다는 점입니다. Service는 label selector라는 조건을 선언할 뿐이고, 그 조건에 맞는 Pod의 실제 IP 목록은 **Endpoints(또는 EndpointSlice)**라는 별도 객체에 채워집니다. 이 분리가 운영과 디버깅의 출발점입니다.

selector → Endpoints → kube-proxy #

Service가 트래픽을 실어 나르기까지의 경로는 세 단계입니다. CKA에서 “Service가 안 된다"는 문제는 거의 항상 이 셋 중 하나가 끊긴 것입니다.

단계주체하는 일
1. 선택Service의 selector어떤 label을 가진 Pod를 대상으로 삼을지 선언
2. 채움endpoints controllerselector에 맞고 Ready인 PodIP를 Endpoints에 기록
3. 구현kube-proxyEndpoints를 노드의 iptables/IPVS 규칙으로 변환
  1. Serviceselector: app=web 같은 조건을 선언합니다.
  2. endpoints controller가 그 조건에 맞고 Ready 상태인 Pod의 IP를 찾아 Endpoints 객체에 채웁니다. readiness probe를 통과하지 못한 Pod는 여기서 빠집니다.
  3. 각 노드의 kube-proxy가 Endpoints를 보고 iptables(또는 IPVS) 규칙을 깔아, ClusterIP로 온 패킷을 실제 PodIP 중 하나로 DNAT합니다.

따라서 ClusterIP는 어떤 프로세스가 listen하는 실제 IP가 아니라, kube-proxy가 노드 커널에 심어 둔 규칙이 가로채는 가상 IP입니다. 이 점을 알면 ping ClusterIP가 안 되는 것이 정상이라는 사실도 자연스럽습니다.

Service 타입 네 가지 #

타입접근 범위동작주 용도
ClusterIP클러스터 내부가상 IP + DNS, 내부 부하 분산기본값. 내부 통신
NodePort모든 노드의 포트ClusterIP에 더해 각 노드의 30000〜32767 포트를 연다외부 노출(단순)
LoadBalancer외부 LB IPNodePort에 더해 클라우드 LB를 프로비저닝클라우드 외부 노출
ExternalNameDNS 별칭selector 없이 CNAME만 반환클러스터 밖 서비스 참조

상위 타입은 하위 타입을 포함합니다. NodePort는 ClusterIP를 그대로 갖고 거기에 노드 포트를 더하며, LoadBalancer는 NodePort 위에 클라우드 로드밸런서를 얹습니다. ExternalName만 결이 다릅니다. selector도 Endpoints도 없이 DNS 단계에서 외부 도메인의 CNAME을 돌려줄 뿐입니다.

port / targetPort / nodePort 구분 #

세 포트 필드를 헷갈리면 트래픽이 엉뚱한 데로 갑니다.

  • port: Service 자신이 ClusterIP에서 여는 포트. 클라이언트가 서비스명:port로 접속합니다.
  • targetPort: 트래픽이 최종 도달하는 Pod의 컨테이너 포트. 생략하면 port와 같은 값으로 간주합니다.
  • nodePort: NodePort/LoadBalancer에서 각 노드가 외부로 여는 포트(30000〜32767). 생략하면 범위에서 자동 할당합니다.

흐름으로 보면 외부에서 노드IP:nodePort → 내부에서 ClusterIP:port → 최종 PodIP:targetPort 순서입니다.

ClusterIP 예제 #

가장 기본인 ClusterIP를 YAML로 보겠습니다. app=web label을 가진 Pod로 80번 트래픽을 8080번 컨테이너 포트에 전달합니다.

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: ClusterIP        # 생략해도 기본값
  selector:
    app: web             # 이 label을 가진 Pod를 대상으로
  ports:
    - port: 80           # Service가 여는 포트 (ClusterIP:80)
      targetPort: 8080   # Pod의 컨테이너 포트

만든 뒤에는 selector가 실제로 Pod를 잡았는지 Endpoints로 확인합니다.

k get svc web
k get endpoints web        # 또는 k get endpointslices

# 클러스터 내부에서 접속 테스트 (임시 Pod)
k run tmp --image=busybox:1.36 --rm -it --restart=Never -- \
  wget -qO- http://web:80

k get endpoints web에 PodIP가 한 줄이라도 나오면 selector가 제대로 물린 것입니다. <none>이면 트래픽이 갈 곳이 없습니다.

NodePort 예제 #

외부에서 노드 IP로 직접 접근해야 할 때 NodePort를 씁니다.

apiVersion: v1
kind: Service
metadata:
  name: web-np
spec:
  type: NodePort
  selector:
    app: web
  ports:
    - port: 80           # ClusterIP 포트
      targetPort: 8080   # Pod 컨테이너 포트
      nodePort: 30080    # 모든 노드가 여는 외부 포트 (30000〜32767)

이제 클러스터 안에서는 web-np:80으로, 클러스터 밖에서는 <아무 노드 IP>:30080으로 같은 Pod에 닿습니다. nodePort를 명시하지 않으면 30000〜32767 범위에서 자동으로 하나 골라 줍니다. 시험에서 특정 포트를 요구하면 반드시 명시해야 합니다.

k get svc web-np
curl http://<노드IP>:30080

k expose: 명령 한 줄로 Service 만들기 #

시험에서는 YAML을 손으로 쓰는 것보다 k exposek create service가 빠를 때가 많습니다. 기존 Deployment 앞에 Service를 다는 가장 빠른 방법입니다.

# Deployment를 ClusterIP로 노출 (selector는 Deployment의 label에서 자동 추출)
k expose deployment web --port=80 --target-port=8080

# NodePort로
k expose deployment web --port=80 --target-port=8080 --type=NodePort

# 만들기 전에 YAML만 확인하고 싶을 때
k expose deployment web --port=80 --target-port=8080 $do

k expose의 큰 장점은 selector를 자동으로 맞춰 준다는 점입니다. 손으로 selector를 쓰다 오타를 내면 Endpoints가 비는데, expose는 대상 객체의 label을 그대로 가져오므로 그 실수가 없습니다. 다만 nodePort를 특정 값으로 고정하는 옵션은 없으니, 고정이 필요하면 $do로 YAML을 뽑은 뒤 nodePort 한 줄을 넣어 적용하겠습니다.

headless Service #

clusterIP: None으로 두면 headless Service가 됩니다. 가상 IP를 받지 않고, DNS 조회 시 ClusterIP 하나가 아니라 각 Pod의 IP를 직접 돌려줍니다.

apiVersion: v1
kind: Service
metadata:
  name: web-hl
spec:
  clusterIP: None        # headless
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 8080

kube-proxy가 끼지 않으므로 부하 분산도 DNAT도 없습니다. 클라이언트가 DNS에서 받은 Pod IP 목록을 보고 직접 고릅니다. StatefulSet처럼 개별 Pod를 고유 이름으로 지목해야 하는 워크로드가 headless Service를 짝으로 씁니다. StatefulSet은 #11에서 다뤘습니다.

ExternalName: 클러스터 밖 서비스에 별칭 붙이기 #

ExternalName은 selector도 Endpoints도 없습니다. CoreDNS가 이 Service 이름을 외부 도메인의 CNAME으로 돌려줄 뿐입니다.

apiVersion: v1
kind: Service
metadata:
  name: db
spec:
  type: ExternalName
  externalName: db.prod.example.com

클러스터 안의 앱은 db.<네임스페이스>.svc.cluster.local을 찾고, CoreDNS가 그것을 db.prod.example.com으로 안내합니다. 매니지드 데이터베이스처럼 클러스터 밖 엔드포인트를 Service 이름으로 추상화할 때 유용합니다.

운영: Service가 안 될 때 추적하는 순서 #

CKA 트러블슈팅에서 “앱에 접속이 안 된다"는 문제는 대부분 이 경로를 따라 끊긴 곳을 찾으면 풀립니다. 위에서부터 차례로 확인하겠습니다.

  1. Service가 존재하고 타입/포트가 맞는가
k get svc web
k describe svc web        # Selector, Port, TargetPort 확인
  1. Endpoints에 PodIP가 채워졌는가. 가장 흔한 실패 지점입니다.
k get endpoints web

ENDPOINTS<none>이면 트래픽이 갈 곳이 없습니다. 원인은 거의 둘입니다.

  • selector 불일치: Service의 selector와 Pod의 label이 다릅니다. k describe svc의 Selector와 k get pods --show-labels를 나란히 놓고 비교하겠습니다.
  • Pod가 NotReady: readiness probe를 통과하지 못한 Pod는 Endpoints에서 빠집니다. k get pods로 READY 열을 확인하겠습니다.
# selector와 실제 label을 직접 대조
k describe svc web | grep -i selector
k get pods --show-labels | grep web
  1. targetPort가 컨테이너가 listen하는 포트와 같은가

Endpoints에 IP가 있는데도 연결이 안 되면, targetPort가 컨테이너의 실제 포트와 다른 경우가 많습니다. Endpoints에 찍힌 IP:포트로 Pod에서 직접 찔러 봅니다.

k get endpoints web -o wide
k run tmp --image=busybox:1.36 --rm -it --restart=Never -- \
  wget -qO- http://<PodIP>:<targetPort>
  1. kube-proxy가 도는가

ClusterIP는 되는데 NodePort만 안 되거나 노드별로 결과가 다르면, 해당 노드의 kube-proxy를 의심합니다.

k get pods -n kube-system -l k8s-app=kube-proxy -o wide
k logs -n kube-system <kube-proxy-pod>

이 순서를 외워 두면 “Service가 안 된다"가 막연한 문제가 아니라 체크리스트가 있는 문제로 바뀝니다.

selector 불일치를 만들어 보고 고치기 #

가장 잦은 실수를 직접 재현해 보겠습니다. Pod label은 app=web인데 Service selector를 app=webb로 잘못 적은 상황입니다.

# Endpoints가 비어 있다 → selector 불일치 의심
k get endpoints web
# NAME   ENDPOINTS   AGE
# web    <none>      30s

# selector를 올바른 label로 교정
k patch svc web -p '{"spec":{"selector":{"app":"web"}}}'

# 다시 채워졌는지 확인
k get endpoints web

selector를 고치는 즉시 endpoints controller가 PodIP를 다시 채우고, kube-proxy가 규칙을 갱신합니다. 별도 재시작이 필요 없습니다.

시험 포인트 #

  • Endpoints를 먼저 본다. Service 문제의 첫 명령은 거의 항상 k get endpoints입니다. 비어 있으면 selector나 readiness, 차 있으면 targetPort나 kube-proxy를 의심합니다.
  • port / targetPort / nodePort를 구분한다. port는 Service, targetPort는 Pod, nodePort는 노드입니다. 셋이 헷갈리면 오답으로 직결됩니다.
  • NodePort 범위는 30000〜32767. 시험에서 특정 nodePort를 요구하면 명시하고, 범위를 벗어나면 거부됩니다.
  • k expose가 selector 오타를 막아 준다. 기존 워크로드를 노출할 때는 손으로 selector를 쓰지 말고 expose로 자동 추출하는 편이 빠르고 안전합니다.
  • headless는 clusterIP: None. 가상 IP 없이 PodIP를 직접 반환하는 동작과 StatefulSet과의 짝을 묻습니다.
  • ClusterIP에 ping은 안 된다. ClusterIP는 가상 IP라 ICMP에 응답하지 않습니다. 검증은 ping이 아니라 wget/curl로 합니다.

정리 #

이번 글에서 잡은 것:

  • Service는 흔들리는 Pod 집합 앞의 안정된 가상 IP,DNS,부하 분산 추상화입니다.
  • 동작은 selector(Service) → Endpoints(endpoints controller) → 규칙(kube-proxy) 세 단계입니다. Service는 Pod를 직접 가리키지 않습니다.
  • 타입은 ClusterIP(기본,내부) / NodePort(노드 포트 30000〜32767) / LoadBalancer(클라우드 LB) / ExternalName(CNAME) 네 가지이며 상위가 하위를 포함합니다.
  • 포트는 **port(Service) / targetPort(Pod) / nodePort(노드)**로 구분합니다.
  • clusterIP: None은 headless로 PodIP를 직접 반환하고, k expose는 selector를 자동으로 맞춰 줍니다.
  • Service가 안 되면 Endpoints → selector/readiness → targetPort → kube-proxy 순서로 추적합니다.

다음: Networking 2 #

Service로 Pod 집합에 안정된 입구를 만들었습니다. 하지만 NodePort는 포트 번호가 지저분하고, LoadBalancer는 서비스마다 LB를 하나씩 띄워 비쌉니다. HTTP 레벨에서 호스트,경로 기반으로 여러 서비스를 한 입구에 모으는 도구가 필요합니다.

#19 Networking 2: Ingress, IngressClass, TLS에서는 Ingress가 어떻게 L7 라우팅을 하는지, IngressClass로 어떤 컨트롤러가 처리할지 고르는 방법, 그리고 TLS 인증서를 Secret으로 붙여 HTTPS를 종단하는 법까지 운영 관점으로 정리하겠습니다.

X