K8s 기초 #5 Service — ClusterIP / NodePort / LoadBalancer
#4 Deployment와 ReplicaSet에서 Pod 3개의 IP가 매번 바뀐다는 점을 확인했습니다. 이번 글에서는 그 문제를 해결하는 추상화인 Service를 다루겠습니다. 안정적인 가상 IP와 DNS 이름, selector가 만든 백엔드 묶음, 그리고 ClusterIP / NodePort / LoadBalancer 세 가지 노출 방식을 함께 정리하겠습니다.
이번 시리즈는 K8s 기초 7편입니다.
- #1 쿠버네티스란 — 왜 컨테이너 오케스트레이터가 필요한가
- #2 로컬 환경 — minikube / kind / Docker Desktop k8s
- #3 kubectl과 첫 Pod
- #4 Deployment와 ReplicaSet — 선언형 배포와 롤링 업데이트
- #5 Service — ClusterIP / NodePort / LoadBalancer ← 이번 글
- #6 ConfigMap / Secret
- #7 Namespace와 라벨
이번 글의 끝에서는 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이 객체가 디버깅의 1번 무기입니다. “Service에 트래픽이 안 가는 것 같아” 라는 증상이 나오면 가장 먼저 보는 곳이 여기입니다.
kubectl get endpoints webNAME ENDPOINTS AGE
web <none> 1mENDPOINTS가 비어 있다면, Service의 selector가 어느 Pod에도 매칭하지 않고 있다는 뜻입니다. 둘 중 하나입니다 — selector 라벨이 오타거나, 매칭할 Pod가 그 네임스페이스에 없거나. kubectl get pods --show-labels로 실제 Pod의 라벨을 확인하고 selector와 맞춰 보면 답이 나옵니다.
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의 상위 개념에 가깝습니다.
로컬,온프렘 환경에서 #
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를 호스트/경로로 라우팅하는 상위 추상화이고, 이번 K8s 기초 시리즈 범위 밖이므로 중급 시리즈에서 따로 다루겠습니다. 이번 글에서는 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들을 그대로 돌려줍니다. StatefulSet처럼 클라이언트가 Pod별로 직접 닿아야 하는 경우의 짝입니다. 일반 웹 서비스에서는 거의 안 씁니다.
kube-proxy — 그래서 누가 트래픽을 흘리는가 #
여기까지 따라왔다면 한 가지가 살짝 마음에 걸려요 — Service의 ClusterIP 10.96.142.31은 어느 노드에도 실제로 붙어 있지 않은 IP입니다. ip addr로 어느 노드를 뒤져도 그 IP가 없습니다. 그런데도 Pod 안에서 그 IP로 패킷을 보내면 어딘가로 도착합니다. 누가 흘려 주는 걸까요.
답은 각 노드에서 도는 kube-proxy 라는 시스템 컴포넌트입니다. #1의 컨트롤 플레인 그림에는 안 나왔지만, 모든 워커 노드에 한 개씩 떠 있는 데몬입니다.
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 등)은 K8s 중급 네트워킹 주제로 미뤄 두겠습니다.
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만 한 줄 남아 있는 게 정상입니다 — 그건 클러스터가 자체적으로 들고 있는 거라 우리가 지울 대상이 아닙니다.
정리 #
이번 글에서 잡은 흐름을 정리합니다.
- Pod IP는 임시. 같은 라벨의 N개 Pod에 안정 IP,DNS,부하 분산을 한 번에 얹어 주는 추상화가 Service다.
- Service 매니페스트의 척추는
apiVersion: v1/kind: Service/spec.type/spec.selector/spec.ports. selector는 #4의 Deployment template 라벨과 반드시 매칭되도록 둔다. - selector의 결과는 Endpoints / EndpointSlice 객체로 자동 정리됩니다.
kubectl get endpoints <svc>가 비어 있으면 selector,라벨이 어긋난 첫 의심점. - 타입은 셋 — ClusterIP(기본, 클러스터 내부 전용), NodePort(
<NodeIP>:30000–32767로 외부 노출), LoadBalancer(클라우드 LB 자동 생성). 부가로ExternalName과 Headless(clusterIP: None). - Service의 가상 IP는 어느 노드에도 실재하지 않는다. 노드의 **
kube-proxy**가 iptables/IPVS 규칙으로 DNAT 해서 Pod IP로 흘려 준다 — 분산된 가상 LB. - 짧은 이름
<svc>가 풀리는 건kube-system의 CoreDNS가 Service마다 A 레코드를 자동으로 만들기 때문. FQDN은<svc>.<ns>.svc.cluster.local.
다음 — ConfigMap / Secret #
여기까지 와도 매니페스트 안에 한 가지가 여전히 어색하게 남아 있습니다 — 이미지 태그, 포트, 도메인 같은 값이 매니페스트에 직접 적힌 채라는 점. 환경(개발/스테이징/운영)에 따라 달라져야 할 값들과, 비밀번호처럼 평문으로 매니페스트에 두면 안 되는 값들을 매니페스트 본체에서 떼어 내는 일이 다음 주제입니다.
#6 ConfigMap / Secret에서는 (1) ConfigMap에 환경 설정값을 모아 Pod에 환경변수,볼륨으로 주입하는 모양, (2) Secret이 ConfigMap과 무엇이 다른지(그리고 base64는 암호화가 아니라는 한 줄), (3) 이번 글의 web Deployment에 설정값 한 묶음을 외부 객체로 떼어 내 보는 한 사이클까지 다루겠습니다.