Service
Pod IP가 임시라는 문제를 푸는 추상화 — Service. 안정된 ClusterIP · selector · Endpoints / EndpointSlice · 그리고 ClusterIP · NodePort · LoadBalancer 세 타입의 선택 기준, kube-proxy의 DNAT, CoreDNS의 짧은 이름 풀이까지 한 사이클로 다룹니다.
4장 Deployment와 ReplicaSet에서 Pod 3개가 자동으로 살아 떠 있는 모양까지는 만들었습니다. 다만 그 3개의 IP가 매번 바뀐다는 점이 마음에 걸려 있습니다. 이번 챕터에서는 그 문제를 해결하는 추상화인 Service를 다룹니다. 안정적인 가상 IP와 DNS 이름, selector가 만든 백엔드 묶음, 그리고 ClusterIP / NodePort / LoadBalancer 세 가지 노출 방식을 함께 정리합니다.
이번 챕터의 끝에서는 Pod 앞단에 안정된 진입점을 두는 첫 매니페스트가 손에 들어옵니다. 클러스터 안에서 Pod 끼리 이름으로 서로를 부르는 모양도, 외부 브라우저에서 노드 포트로 직접 들어오는 모양도, 클라우드 환경에서 외부 LB가 자동으로 붙는 모양도 한 줄짜리 차이로 갈라집니다.
Pod IP의 한계 — 왜 Service가 필요한가 #
4장의 끝까지 따라왔다면 머릿속 그림은 이렇습니다 — app: web 라벨이 붙은 nginx Pod 3개가 떠 있고, 각자 10.244.0.5, 10.244.0.6, 10.244.0.7 같은 클러스터 내부 IP를 갖고 있습니다. 이 상태에서 한 가지를 더 해 보고 싶어집니다 — 같은 클러스터 안의 다른 Pod에서 저 3개에 HTTP 요청을 보내거나, 노트북 브라우저에서 한 번 열어 보는 일입니다.
그런데 막상 해 보면 네 가지 문제가 한꺼번에 걸립니다.
- Pod IP는 임시 (ephemeral)입니다. Pod가 한 번 재생성되면 새 IP가 붙습니다. 어제 적어 둔
10.244.0.5는 오늘은 없는 IP 일 수 있습니다. 클라이언트 코드에 IP를 고정해 두고 부르는 길은 처음부터 닫혀 있습니다. - 3개의 Pod 사이에 부하 분산이 없습니다. 한 Pod IP를 골라서 부르면 그 Pod만 일을 하고 나머지 둘은 놀게 됩니다. 누가 트래픽을 N개의 Pod에 골고루 흩뿌려 줘야 합니다.
- 서비스 디스커버리가 없습니다. 클라이언트 Pod 입장에서 “그 web 서비스의 현재 IP가 뭐냐"를 매번 어디에 물어봐야 하는지 모호합니다. IP가 아니라 이름으로 부를 수 있는 길이 필요합니다.
- 외부 트래픽 진입로가 없습니다. 클러스터 내부 IP는 노트북 브라우저에서 보이지 않습니다. 외부의 무언가를 안의 Pod로 흘리는 입구가 따로 마련돼 있어야 합니다.
이 넷을 한 번에 푸는 추상화가 Service입니다. 매니페스트 한 장을 적으면 K8s가 안정된 가상 IP를 잡아 주고, 그 IP가 부하 분산기 역할을 하면서 selector로 묶인 Pod 들에게 트래픽을 흘려 주고, 같은 클러스터의 다른 Pod에서 이름으로 부를 수 있는 DNS 레코드까지 자동으로 만들어 줍니다.
Service — 안정 IP + selector + DNS #
Service 매니페스트 한 장이 만들어 내는 결과를 셋으로 나눕니다.
- 안정된 가상 IP (ClusterIP) — 클러스터가 살아 있는 동안 바뀌지 않는 IP입니다. Pod가 죽고 살고와 무관하게 같은 IP가 유지됩니다.
- selector로 묶인 Pod 그룹 —
spec.selector라벨에 매칭되는 Pod 들이 그 Service의 백엔드가 됩니다. Pod가 새로 떠도 라벨만 맞으면 자동으로 합류하고, 죽으면 자동으로 제외됩니다. - DNS 이름 —
<svc>.<ns>.svc.cluster.local형태의 FQDN이 자동으로 생깁니다. 같은 네임스페이스 안에서는<svc>짧은 이름만으로도 부를 수 있습니다.
머릿속 그림은 이렇게 두면 편합니다.
┌──────────────────────────────┐
│ Service: web │ selector: app=web
│ ClusterIP: 10.96.x.x │ DNS: web.default.svc.cluster.local
└──────────────┬───────────────┘
│ 트래픽 분배
┌──────────┼──────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Pod-1 │ │ Pod-2 │ │ Pod-3 │ app=web
│.0.5 │ │.0.6 │ │.0.7 │ (Pod IP 는 임시)
└────────┘ └────────┘ └────────┘위 그림에서 핵심은 — 클라이언트는 가운데의 Service IP나 이름만 바라보면 되고, 아래 Pod 들이 죽고 살고는 K8s가 알아서 갱신해 준다는 점입니다. Service가 들고 있는 IP는 안정적이고, 그 뒤의 Pod IP 들은 임시입니다. 둘이 분리돼 있어야 무중단 운영이 가능합니다.
Endpoints / EndpointSlice — selector의 결과 #
Service의 selector가 매칭한 Pod 들의 IP · 포트 목록은 K8s가 별도 객체에 정리해 둡니다. 이 객체가 Endpoints (또는 1.21+ 부터 권장되는 EndpointSlice)입니다. 사람이 직접 만드는 일은 거의 없고, Service를 만들면 K8s가 자동으로 채워 줍니다.
kubectl get endpoints webNAME ENDPOINTS AGE
web 10.244.0.5:80,10.244.0.6:80,10.244.0.7:80 30sENDPOINTS 컬럼에 Pod IP 들이 그대로 나열돼 있습니다. Pod가 한 개 죽으면 이 목록에서 곧 사라지고, 새로 떠오른 Pod가 라벨에 맞으면 자동으로 합류합니다.
1.21+ 부터는 EndpointSlice가 권장됩니다. 한 Service의 백엔드가 많아질 때 한 객체가 너무 비대해지는 문제를 풀려고 도입된 모양입니다. 큰 차이는 없고, 사용자 입장에서는 둘 다 kubectl get으로 볼 수 있습니다.
kubectl get endpointslices -l kubernetes.io/service-name=webNAME ADDRESSTYPE PORTS ENDPOINTS AGE
web-abc12 IPv4 80 10.244.0.5,10.244.0.6,10.244.0.7 30s이 객체가 Service 디버깅의 1차 출발점입니다. **“Service에 트래픽이 안 가는 것 같다”**는 증상이 나오면 가장 먼저 보는 곳이 여기입니다.
kubectl get endpoints webNAME ENDPOINTS AGE
web <none> 1mENDPOINTS가 비어 있다면, Service의 selector가 어느 Pod에도 매칭하지 않고 있다는 뜻입니다. 둘 중 하나입니다 — selector 라벨이 오타거나, 매칭할 Pod가 그 네임스페이스에 없는 경우입니다. kubectl get pods --show-labels로 실제 Pod의 라벨을 확인하고 selector와 맞춰 보면 답이 나옵니다. 진단 트리의 완성된 버전은 27장 kubectl 디버깅 패턴에서 정리합니다.
ClusterIP — 클러스터 내부 전용 #
가장 자주 쓰는 기본 타입부터 시작합니다. Service의 spec.type을 적지 않으면 자동으로 ClusterIP입니다. 클러스터 안에서만 닿는 가상 IP를 잡아 주는 모양입니다.
4장에서 띄운 app: web Deployment가 그대로 떠 있다고 가정하고, 그 앞단에 Service를 하나 붙여 봅니다. 파일 이름은 web-svc.yaml로 둡니다.
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: ClusterIP
selector:
app: web
ports:
- port: 80
targetPort: 80매니페스트의 척추는 3장에서 본 네 필드 그대로 — apiVersion / kind / metadata / spec입니다. Service는 apps/v1이 아니라 코어 그룹의 **v1**이라는 점만 주의하면 됩니다. Deployment와 자주 헷갈리는 부분입니다.
spec 안에서 새로 만나는 부분은 셋입니다.
type—ClusterIP/NodePort/LoadBalancer/ExternalName중 하나입니다. 적지 않으면ClusterIP입니다.selector— 어떤 라벨의 Pod를 백엔드로 잡을지 정합니다. 위에서는app: web으로 두었습니다. 4장의 Deployment template 라벨과 일치하게 둔 게 핵심입니다.ports— 포트 매핑 목록입니다. 한 Service가 여러 포트를 한꺼번에 노출할 수도 있고, 위처럼 한 줄만 적어도 됩니다.
port vs targetPort #
ports 아래의 두 필드를 한 줄로 짚어 둡니다.
port— Service가 듣는 포트입니다. 클라이언트가 두드리는 곳입니다. 위 매니페스트라면web:80으로 들어옵니다.targetPort— 백엔드 Pod의 컨테이너가 듣는 포트입니다. nginx 컨테이너가 80번을 듣고 있으니 80입니다.
둘이 같은 숫자라 헷갈리기 쉽지만, 따로 두는 이유가 있습니다. 예를 들어 컨테이너는 8080을 듣게 두고 Service는 표준 80으로 노출하고 싶다면 port: 80, targetPort: 8080처럼 다르게 적으면 됩니다. 이런 분리가 있어서 Service가 일종의 가벼운 포트 매핑 계층 역할도 합니다.
apply와 결과 확인 #
매니페스트를 클러스터에 반영합니다.
kubectl apply -f web-svc.yamlservice/web createdkubectl get svcNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 2d
web ClusterIP 10.96.142.31 <none> 80/TCP 10s컬럼명을 한 줄로 짚어 두면 — NAME / TYPE / CLUSTER-IP / EXTERNAL-IP / PORT(S) / AGE입니다. 본 책 끝까지 자주 만납니다. kubernetes 줄은 클러스터가 자체적으로 들고 있는 API 서버용 Service 라 신경 안 써도 됩니다. 새로 보이는 건 web 한 줄입니다. CLUSTER-IP 10.96.142.31이 잡혔고, EXTERNAL-IP는 <none>입니다 — 클러스터 안에서만 닿는다는 뜻입니다.
(IP의 10.96.0.0/12 영역은 kubeadm 기본 서비스 CIDR입니다. 환경마다 다르게 잡힐 수 있습니다. minikube · kind는 비슷하고, EKS · GKE 같은 매니지드는 자기 기본값이 따로 있습니다.)
클러스터 안에서 호출해 보기 #
ClusterIP의 핵심 검증은 다른 Pod에서 이 Service를 부를 수 있는가입니다. 임시 디버그용 Pod 한 개를 띄워 안에서 curl을 한 번 두드려 봅시다.
kubectl run tmp --rm -it --image=curlimages/curl -- sh--rm은 종료할 때 Pod를 자동으로 지우는 옵션이고, -it은 인터랙티브 + TTY입니다. 들어가서 세 가지 모양으로 호출해 봅니다.
/ $ curl -s http://web | head -1
<!DOCTYPE html>
/ $ curl -s http://web.default.svc.cluster.local | head -1
<!DOCTYPE html>
/ $ curl -s http://10.96.142.31 | head -1
<!DOCTYPE html>세 길이 모두 같은 곳을 가리킵니다.
- 짧은 이름
web— 같은 네임스페이스 (default) 안에서는 Service 이름만으로도 닿습니다. 가장 자주 쓰는 형태입니다. - FQDN
web.default.svc.cluster.local— 다른 네임스페이스의 Service를 부를 때, 또는 모호함을 없애고 싶을 때 쓰는 정식 이름입니다. - ClusterIP
10.96.142.31— 가상 IP를 직접 두드려도 되지만, 이 IP를 외울 일은 거의 없습니다. DNS로 부르는 게 정공법입니다.
같은 명령을 여러 번 두드려 보면 매번 응답이 같은 nginx 환영 페이지지만, 실제로는 K8s가 요청마다 백엔드 Pod 3개 중 하나를 골라 흘리고 있습니다. 부하 분산은 따로 설정 안 해도 기본 동작입니다. 어떤 Pod가 실제로 응답했는지 확인하고 싶다면 nginx access log를 한 번 열어 보면 됩니다 — 세 Pod의 로그에 골고루 요청이 떨어지는 것을 볼 수 있습니다.
exit로 임시 Pod를 빠져나오면 --rm 덕분에 자동으로 정리됩니다. 운영에서 클러스터 내부 통신은 거의 항상 이 ClusterIP 모양입니다. 백엔드 ↔ DB, 백엔드 ↔ Redis, 마이크로서비스끼리의 호출 — 모두 ClusterIP로 묶입니다.
NodePort — 노드 IP의 특정 포트로 노출 #
ClusterIP는 클러스터 안에서만 닿는다고 했습니다. 외부에서 닿게 만드는 가장 단순한 방법이 NodePort입니다. 클러스터의 모든 노드에서 같은 포트 (기본 30000~32767 범위)를 열고, 그 포트로 들어오는 트래픽을 같은 Service로 흘립니다.
매니페스트는 ClusterIP에 두 줄만 더 적으면 됩니다.
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: NodePort
selector:
app: web
ports:
- port: 80
targetPort: 80
nodePort: 30080type: NodePort로 바꿨고, ports 아래에 nodePort: 30080을 추가했습니다. nodePort를 적지 않으면 K8s가 30000~32767 범위에서 자동으로 하나 골라 줍니다. 직접 적을 때는 그 범위 안의 값이어야 합니다.
kubectl apply -f web-svc.yamlservice/web configuredkubectl get svc webNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web NodePort 10.96.142.31 <none> 80:30080/TCP 5m달라진 부분은 두 곳입니다 — TYPE이 NodePort로, PORT(S)가 80:30080/TCP로 바뀌었습니다. 앞쪽 80이 Service의 port (클러스터 안에서 두드리는 포트), 뒤쪽 30080이 노드의 NodePort입니다. 이제 클러스터 안에서는 여전히 web:80으로 닿고, 클러스터 밖에서는 <NodeIP>:30080으로 닿습니다.
curl http://<NodeIP>:30080<NodeIP> 부분에는 워커 노드의 외부 IP를 넣으면 됩니다. 로컬 환경별 모양이 살짝 다릅니다.
- kind — 노드는 도커 컨테이너 안이라 호스트에서 바로 안 닿습니다. 클러스터 만들 때
extraPortMappings로 30080을 호스트 쪽에 노출하거나,kubectl port-forward로 우회합니다. - minikube —
minikube service web --url로 접근 URL을 받을 수 있습니다. - Docker Desktop k8s — 노드 = 호스트 자체라
localhost:30080으로 바로 닿습니다.
운영에서 NodePort를 직접 클라이언트에 노출하는 일은 드뭅니다. 포트 번호가 30000번대로 어색하고, 노드가 추가 / 제거될 때 외부 클라이언트가 IP 목록을 따라가야 하기 때문입니다. 보통은 그 위에 LoadBalancer나 Ingress가 얹혀 있고, 안쪽에서 NodePort를 사용하는 모양입니다. NodePort 자체는 로컬 개발에서 외부 접근을 빠르게 확인하거나, 디버깅용으로 잠깐 열어 두거나 할 때 유용합니다.
LoadBalancer — 클라우드 LB와 통합 #
운영에서 외부에 노출하는 가장 흔한 모양이 LoadBalancer입니다. type: LoadBalancer 한 줄을 적으면 K8s가 클라우드 제공자 (AWS ELB, GCP LB, Azure LB 등)에 외부 LB를 자동으로 만들어 달라고 요청합니다. 만들어진 LB의 외부 IP가 Service의 EXTERNAL-IP 컬럼에 차오릅니다.
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 80
targetPort: 80kubectl apply -f web-svc.yaml클라우드 환경에서 #
EKS · GKE · AKS 같은 매니지드 클러스터에서 위 매니페스트를 적용하면 보통 1~2분 사이에 외부 LB가 만들어집니다.
kubectl get svc webNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web LoadBalancer 10.96.142.31 <pending> 80:31523/TCP 20sNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web LoadBalancer 10.96.142.31 a1b2c3d4.elb.. 80:31523/TCP 2mEXTERNAL-IP가 <pending>에서 실제 IP / DNS 이름으로 바뀝니다. 그 주소가 외부 진입점입니다. AWS 면 ELB DNS 이름, GCP 면 IP 주소로, 환경마다 형태가 살짝 다릅니다. PORT(S)에 NodePort 31523도 같이 보이는 게 흥미롭습니다 — LoadBalancer 안쪽에서는 NodePort를 자동으로 잡고, 클라우드 LB가 그 NodePort로 트래픽을 흘려 보내는 모양입니다. 그래서 LoadBalancer는 NodePort의 상위 개념에 가깝습니다.
이 모양은 4부 EKS 실전 (22장 앱 배포 골격)에서 ALB와 함께 본격적으로 다룹니다.
로컬 · 온프레미스 환경에서 #
kind, 단독 minikube, 클라우드 컨트롤러가 없는 일반 베어메탈 클러스터에서는 위 매니페스트를 적용해도 EXTERNAL-IP가 영영 <pending> 상태에 머뭅니다. 외부 LB를 만들어 줄 누가 없기 때문입니다.
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web LoadBalancer 10.96.142.31 <pending> 80:31523/TCP 5m이 빈 부분을 채우려고 나온 게 MetalLB (베어메탈용), cloud-provider-kind (kind 전용) 같은 도구들입니다. 설치하면 그 도구가 클라우드 컨트롤러처럼 동작해서 EXTERNAL-IP를 채워 줍니다. 이름만 짚어 두고 깊은 설치는 본 챕터 범위 밖으로 두겠습니다.
요점은 한 줄입니다 — **운영의 외부 진입점은 거의 항상 LoadBalancer 또는 그 위의 Ingress**입니다. Ingress는 한 LoadBalancer 뒤에 여러 Service를 호스트 · 경로로 라우팅하는 상위 추상화이고, 10장 Ingress와 Ingress Controller에서 본격적으로 다룹니다. 본 챕터에서는 LoadBalancer 까지가 끝점입니다.
Service 타입 한 표 #
지금까지 셋과 자주 만나는 둘을 한 표로 정리합니다.
| 타입 | 외부 노출 | 주된 용도 |
|---|---|---|
ClusterIP (기본) | 없음 (클러스터 내부 전용) | 백엔드 ↔ DB, 마이크로서비스 간 통신 |
NodePort | <NodeIP>:<30000 ~ 32767> | 로컬 개발, 디버깅용 외부 접근, LB의 안쪽 구현 |
LoadBalancer | 클라우드 LB의 외부 IP / DNS | 운영 외부 진입점. 클라우드 또는 MetalLB 등 필요 |
ExternalName | 없음 (DNS CNAME만) | 클러스터 내부 이름을 외부 도메인에 별칭으로 두기 |
Headless (clusterIP: None) | 없음 (가상 IP 없음) | StatefulSet처럼 Pod 개별 IP가 필요한 경우 |
마지막 두 줄을 한 줄씩 짚어 둡니다.
ExternalName— 매니페스트에type: ExternalName+externalName: db.example.com을 적으면,<svc>.<ns>.svc.cluster.local을 부를 때 K8s 내부 DNS가 외부 도메인의 CNAME으로 응답합니다. selector도 백엔드 Pod도 없는 특수한 모양을 다룹니다. 외부 시스템을 클러스터 내부 이름으로 부르고 싶을 때 씁니다.- Headless Service —
spec.clusterIP: None을 적으면 가상 IP 자체를 잡지 않고, DNS 조회 시 백엔드 Pod IP 들을 그대로 돌려줍니다. 8장 StatefulSet처럼 클라이언트가 Pod 별로 직접 닿아야 하는 경우의 짝입니다. 일반 웹 서비스에서는 거의 안 씁니다.
kube-proxy — 그래서 누가 트래픽을 흘리는가 #
여기까지 따라왔다면 한 가지가 살짝 걸립니다 — Service의 ClusterIP 10.96.142.31은 어느 노드에도 실제로 붙어 있지 않은 IP입니다. ip addr로 어느 노드를 뒤져도 그 IP가 없습니다. 그런데도 Pod 안에서 그 IP로 패킷을 보내면 어딘가로 도착합니다. 누가 흘려 주는 걸까요.
답은 각 노드에서 도는 **kube-proxy**라는 시스템 컴포넌트입니다. 1장의 worker node 컴포넌트로 이미 한 번 등장했던 데몬입니다.
Pod ─▶ 10.96.142.31:80 (가상 IP)
│
▼ iptables/IPVS 규칙으로 DNAT
│
Pod IP 셋 중 하나 ─▶ 10.244.0.5:80
10.244.0.6:80
10.244.0.7:80kube-proxy가 Endpoints / EndpointSlice를 감시하다가, 노드의 iptables (또는 IPVS) 규칙을 자동으로 적용해 둡니다. 그 규칙이 “10.96.142.31:80으로 가는 패킷은 10.244.0.5:80, .0.6:80, .0.7:80 셋 중 하나로 DNAT 한다"라는 내용입니다. Pod가 Service IP로 보낸 패킷은 노드를 떠나기 전에 이 규칙에 잡혀 실제 Pod IP로 바뀝니다.
그래서 Service는 어느 한 노드의 LB가 아니라, 모든 노드에 분산된 가상 LB입니다. 노드마다 같은 규칙이 적용되어 있어서, 어느 Pod가 어느 노드에 떠 있든 같은 ClusterIP로 부르면 똑같이 잘 닿습니다. kube-proxy의 모드는 보통 iptables (기본)나 ipvs이고, 더 깊은 동작과 eBPF 기반 대안 (Cilium 등)은 15장 CNI 깊이에서 다룹니다.
DNS — CoreDNS와 서비스 이름 #
web 같은 짧은 이름이 어떻게 ClusterIP로 풀리는지 한 단락으로 짚어 둡니다. 클러스터의 kube-system 네임스페이스에 CoreDNS라는 DNS 서버가 떠 있습니다 (보통 Pod 두 개로). 이 CoreDNS가 모든 Service에 대해 A 레코드를 자동으로 만들어 둡니다.
기본 도메인은 cluster.local이고, FQDN은 <svc>.<ns>.svc.cluster.local입니다. 같은 네임스페이스 안에서는 짧은 이름 <svc>만 적어도 search 도메인이 알아서 붙어 풀립니다.
nslookup web
# Server: 10.96.0.10
# Address: 10.96.0.10#53
#
# Name: web.default.svc.cluster.local
# Address: 10.96.142.31응답 IP가 우리가 본 ClusterIP와 같다는 점이 핵심입니다. Pod의 /etc/resolv.conf는 K8s가 자동으로 채워 주는데, nameserver에 CoreDNS의 ClusterIP (10.96.0.10 같은 값)가 적혀 있고 search에 <ns>.svc.cluster.local svc.cluster.local cluster.local이 적혀 있습니다. 그래서 짧은 이름이 자동으로 정식 이름으로 확장됩니다.
기본 도메인 cluster.local은 변경 가능합니다 (클러스터 설치 시 옵션). 다만 거의 모든 환경이 기본값을 그대로 쓰므로, 매니페스트나 코드에 적을 때는 cluster.local을 가정해도 무방합니다.
정리·치우기 #
오늘 만든 Service와, 4장에서 떠 있던 Deployment를 함께 깨끗이 지웁니다.
kubectl delete -f web-svc.yamlservice "web" deletedkubectl delete deploy webdeployment.apps "web" deletedkubectl get svc,deploy,pods로 비어 있는지 확인하면 출발점으로 돌아옵니다. kubernetes Service만 한 줄 남아 있는 게 정상입니다 — 그건 클러스터가 자체적으로 들고 있는 Service 라 우리가 지울 대상이 아닙니다.
연습문제 #
- 위 본문대로
web-svc.yaml의type을ClusterIP→NodePort→LoadBalancer순으로 한 번씩 바꾸며kubectl apply해 보세요. 매 단계의kubectl get svc web출력에서TYPE/CLUSTER-IP/EXTERNAL-IP/PORT(S)컬럼이 어떻게 달라지는지를 표로 정리합니다. LoadBalancer 단계에서EXTERNAL-IP가<pending>으로 머물러 있는지, 실제 주소로 바뀌는지에 따라 환경 (로컬 vs 매니지드 클라우드)의 차이가 어디서 갈리는지 한 단락으로 메모합니다. - Service의
spec.selector라벨을 일부러 한 글자 바꿔 보세요 (예:app: web→app: webb).kubectl get endpoints web의ENDPOINTS컬럼이 어떻게 변하는지,kubectl run tmp --rm -it --image=curlimages/curl -- sh안에서curl http://web을 두드렸을 때 어떤 에러가 나는지를 기록합니다. §“Endpoints / EndpointSlice — selector의 결과"의 디버깅 출발점이 어떻게 적용되는지를 정리합니다. - 임시 curl Pod 안에서
nslookup web,nslookup web.default.svc.cluster.local,nslookup kubernetes.default.svc.cluster.local세 가지를 차례로 두드려 보세요. 각 응답 IP가 어디서 왔는지 (CoreDNS · ClusterIP · kubernetes 시스템 Service)를 표로 정리하고, Pod의/etc/resolv.conf안의search도메인 목록이 짧은 이름의 확장에 어떻게 작용하는지를 한 단락으로 메모합니다.
한 줄 요약: Service는 임시 Pod IP의 한계를 푸는 추상화로, 안정 ClusterIP와 selector로 묶인 Pod 그룹, CoreDNS 자동 A 레코드를 한 묶음으로 제공한다. 외부 노출은 NodePort (노드 포트)와 LoadBalancer (클라우드 LB) 두 가지로 갈라지고, 실제 트래픽은 각 노드의 kube-proxy가 iptables · IPVS로 DNAT 해서 Pod IP로 흘린다.
다음 챕터 #
여기까지 와도 매니페스트 안에 한 가지가 여전히 어색하게 남아 있습니다 — 이미지 태그, 포트, 도메인 같은 값이 매니페스트에 직접 적힌 채라는 점입니다. 환경 (개발 / 스테이징 / 운영)에 따라 달라져야 할 값들과, 비밀번호처럼 평문으로 매니페스트에 두면 안 되는 값들을 매니페스트 본체에서 떼어 내는 일이 다음 주제입니다.
6장 ConfigMap과 Secret에서는 ConfigMap에 환경 설정값을 모아 Pod에 환경변수 · 볼륨으로 주입하는 모양, Secret이 ConfigMap과 무엇이 다른지 (그리고 base64는 암호화가 아니라는 한 줄), 본 챕터의 web Deployment에 설정값 한 묶음을 외부 객체로 떼어 내 보는 한 사이클까지 다룹니다. 시크릿의 production 운영 (sealed-secrets · external-secrets · IRSA)은 29장 시크릿 운영에서 본격적으로 다룹니다.