쿠버네티스 네트워킹, 왜 이렇게 헷갈릴까?
또 접속이 안 된다
배포는 분명히 성공했는데, 브라우저에서는 시간 초과 에러만 뜹니다. kubectl로 보면 파드는 멀쩡하게 Running 상태고요. 그런데 왜 접근이 안 되는지 모르겠습니다. 쿠버네티스 네트워킹을 처음 만지는 사람은 거의 다 이 벽을 한 번씩 만납니다.
저도 그랬어요. 몇 년 전 k8s를 처음 만졌을 때, 파드가 떴으니 파드 IP로 바로 접근하면 되겠지 싶었습니다. 당연히 안 되더라고요. 그땐 도대체 왜 이렇게 복잡하게 만들어놨는지 이해가 안 됐습니다. 지금 돌이켜보면 원인은 단순했어요. 파드, 서비스, 노드라는 세 계층이 각각 다른 네트워크 규칙을 갖고 있다는 걸 몰랐던 거죠. 한 덩어리로 보고 접근하니 계속 어긋났던 겁니다.
네트워킹은 결국 "어느 계층에서, 누가, 누구에게 패킷을 흘려보내느냐"의 문제입니다. 이 계층 구분이 머릿속에 잡히면 대부분의 혼란은 정리됩니다.
파드는 어떻게 IP를 받을까
쿠버네티스에서 IP는 컨테이너가 아니라 파드 단위로 할당됩니다. 하나의 파드 안에 컨테이너가 여러 개라면, 그 컨테이너들은 같은 IP를 공유하고 localhost로 서로를 부릅니다.
단일 노드에서는 그림이 단순합니다.
| 엔티티 | IP 주소 | 네트워크 |
|---|---|---|
| 내 노트북 | 192.168.1.10 | 홈 LAN |
| 쿠버네티스 노드 | 192.168.1.2 | 홈 LAN |
| Pod A | 10.244.0.2 | 클러스터 내부 |
| Pod B | 10.244.0.3 | 클러스터 내부 |
쿠버네티스가 뜨면 내부 네트워크(보통 10.244.0.0/16)를 만들고, 여기서 파드들이 IP를 받아 갑니다. 같은 노드의 파드끼리는 바로 통신이 되고요.
여기서 한 가지는 꼭 짚고 가야 합니다. 파드 IP는 언제든 바뀐다는 점이요. 파드가 재시작되거나 롤링 업데이트로 새로 뜨면 IP가 갈아엎어집니다. 그래서 설정 파일이나 코드에 파드 IP를 직접 박아두면, 그건 시한폭탄을 심는 거나 마찬가지입니다. 멀쩡히 돌다가 어느 날 파드 하나 재기동되는 순간 연결이 끊기죠. 이런 류의 장애는 원인 추적이 유난히 짜증납니다. 평소엔 잘 되니까요.
멀티 노드에서 생기는 문제
노드가 하나 더 붙는 순간 그림이 복잡해집니다. 각 노드가 독립적으로 10.244.0.0/16 네트워크를 만들어버리면, 서로 다른 노드의 파드가 같은 IP를 갖는 상황이 생기거든요. 같은 주소가 두 곳에 있으면 라우팅이 성립할 리 없죠.
그래서 쿠버네티스는 네트워크에 대해 몇 가지 요구사항을 못 박습니다.
- 모든 파드는 NAT 없이 다른 모든 파드에 접근 가능해야 함
- 모든 노드는 모든 파드에 접근 가능해야 함
- 모든 파드는 모든 노드에 접근 가능해야 함
재밌는 건, 쿠버네티스 본체는 이 요구사항을 "이렇게 만족시켜라"라고 정해놓고 정작 구현은 직접 하지 않는다는 점입니다. 규격만 던지고 실제 구현은 외부에 위임하는 구조죠. 그 위임을 받는 게 CNI(Container Network Interface) 플러그인입니다. 인터페이스와 구현을 분리해놓은 셈인데, 덕분에 환경에 맞춰 네트워크 레이어를 갈아 끼울 수 있습니다.
CNI 플러그인들
클러스터 설치 시점에 CNI를 하나 고르면, 그 플러그인이 노드별 서브넷 할당, 라우팅 설정, 노드 간 통신을 전부 책임집니다. 네트워크의 실제 배관 공사는 여기서 다 일어난다고 보면 됩니다.
주요 선택지는 이 정도입니다.
| CNI | 특징 |
|---|---|
| Calico | 범용성 좋음, 강력한 네트워크 정책 지원 |
| Flannel | 단순함, '그냥 작동하는' 클래식한 선택 |
| Cilium | eBPF 기반, 고성능, 뛰어난 관측성 |
| Weave Net | 멀티클라우드 설정이 쉬움 |
관리형 쿠버네티스(EKS, GKE, AKS)는 기본 CNI를 깔아주지만 교체도 됩니다. 요즘은 Cilium으로 갈아타는 팀들이 눈에 띄게 늘었더라고요. eBPF 기반이라 관측성이 좋다는 게 큰 이유인 것 같습니다. 네트워크 문제는 결국 "패킷이 어디서 죽었는지"를 보는 게 절반인데, 그걸 잘 보여주는 도구의 가치는 운영을 해본 사람일수록 크게 느낍니다.
CNI가 깔리면 각 노드는 겹치지 않는 고유 서브넷을 받습니다. 노드1은 10.244.0.0/24, 노드2는 10.244.1.0/24 이런 식으로요. 주소가 겹치지 않으니 라우팅이 성립하고, 그제야 모든 파드가 서로 통신할 수 있게 됩니다.
서비스가 필요한 이유
앞에서 말했듯 파드는 언제든 사라지고 새로 뜹니다. IP도 같이 바뀌고요. 그럼 프론트엔드가 백엔드를 어떻게 안정적으로 호출할까요? 매번 바뀌는 주소를 어떻게 따라잡죠?
**서비스(Service)**가 이 문제의 답입니다. 서비스가 하는 일은 단순합니다.
- 레이블 셀렉터로 파드 그룹을 묶고
- 자기 자신은 안 바뀌는 IP와 DNS 이름을 가지며
- 매칭되는 파드들에 트래픽을 로드밸런싱합니다
쉽게 말하면, 변하는 파드 무리 앞에 안 변하는 간판 하나를 세워두는 겁니다. 호출하는 쪽은 간판만 보면 되고, 뒤에서 파드가 몇 개 뜨고 죽는지는 신경 쓸 필요가 없죠. 이 추상화 한 겹이 쿠버네티스 네트워킹의 핵심입니다.
일상적으로 마주칠 서비스 타입은 세 가지입니다.
ClusterIP: 기본 내부 서비스
클러스터 내부에서만 접근 가능한 서비스입니다. 가장 많이 쓰는 타입이죠.
apiVersion: v1
kind: Service
metadata:
name: backend
spec:
type: ClusterIP # 생략해도 됨 (기본값)
ports:
- port: 80 # 서비스가 노출하는 포트
targetPort: 8080 # 파드가 실제 받는 포트
selector:
app: backend # app=backend 레이블 파드 선택
적용하면 이렇게 됩니다.
kubectl apply -f backend-service.yaml
kubectl get svc backend
# NAME TYPE CLUSTER-IP PORT(S) AGE
# backend ClusterIP 10.100.20.4 80/TCP 5s
이제 클러스터 안의 다른 파드에서 이렇게 호출할 수 있습니다.
curl http://backend # DNS로 해결
curl http://10.100.20.4 # IP로도 가능하지만 이름 추천
실무에서는 보통 이런 구조로 깔립니다.
- 프론트엔드 파드 → 백엔드 ClusterIP 서비스
- 백엔드 파드 → Redis ClusterIP 서비스, MySQL ClusterIP 서비스
각 계층이 따로따로 스케일링되고 재배포돼도, 서비스가 안 변하는 엔드포인트를 받쳐주니까 호출하는 쪽 설정은 건드릴 일이 없습니다. MSA를 굴려본 사람이라면 이 "엔드포인트가 안 변한다"는 게 운영에서 얼마나 중요한지 알 겁니다. 서비스 간 의존이 IP로 묶이는 순간 배포가 서로 발목을 잡거든요.
디버깅 팁: 연결이 안 되면 kubectl exec로 파드에 들어가서 nslookup backend, curl -v http://backend 순서로 확인해보세요. DNS는 되는데 curl이 안 되면 셀렉터 매칭 문제일 가능성이 높습니다. 이 순서대로 좁혀가면 원인이 DNS인지, 셀렉터인지, 파드 자체인지 금방 갈립니다.

NodePort: 외부에서 접근 가능하게
모든 노드의 같은 포트를 열어서 외부 접근을 허용하는 서비스입니다.
apiVersion: v1
kind: Service
metadata:
name: myapp-service
spec:
type: NodePort
ports:
- port: 80 # 서비스 포트 (내부)
targetPort: 80 # 파드 포트
nodePort: 30008 # 노드 포트 (외부, 30000-32767)
selector:
app: myapp
NodePort는 포트가 세 개나 얽혀서 처음엔 헷갈립니다.
- nodePort: 외부에서 접근하는 포트 (30008)
- port: 클러스터 내부 서비스 포트 (80)
- targetPort: 파드가 실제 받는 포트 (80)
nodePort를 생략하면 30000-32767 범위에서 자동으로 하나 잡아 갑니다.
kubectl apply -f nodeport-service.yaml
# 외부에서 접근
curl http://192.168.1.2:30008
curl http://192.168.1.3:30008 # 모든 노드에서 같은 포트
파드가 여러 개면 알아서 로드밸런싱되고, 파드가 여러 노드에 흩어져 있어도 어느 노드로 들어가든 상관없습니다. 외부에서 노드 하나만 알면 접근이 되는 셈이죠.
다만 NodePort는 프로덕션 공개 서비스로는 부적절합니다. 30000번대 임의 고번호 포트를 그대로 노출하는 데다, TLS 종료나 호스트 기반 라우팅 같은 건 아예 제공하지 않거든요. 데모나 내부 테스트 용도까지가 적정선이라고 봅니다.
LoadBalancer: 프로덕션급 외부 노출
클라우드 제공업체의 실제 로드밸런서를 프로비저닝하는 서비스입니다.
apiVersion: v1
kind: Service
metadata:
name: voting-app
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 80
selector:
app: voting-app
적용하면 이렇게 외부 IP가 붙습니다.
kubectl get svc voting-app
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# voting-app LoadBalancer 10.100.55.9 34.102.77.14 80:31842/TCP 45s
이제 34.102.77.14로 DNS를 걸면 서비스가 공개됩니다. 클라우드 위에서는 이 한 줄(type: LoadBalancer)이 실제로 클라우드 LB 자원을 프로비저닝하는 명령이 됩니다. 편한 만큼, LB 하나당 비용이 붙는다는 것도 잊으면 안 되고요. 서비스마다 LoadBalancer를 박아두면 LB가 우수수 생기면서 청구서가 조용히 불어납니다. 그래서 실무에서는 보통 Ingress 한 겹을 두고 그 뒤로 서비스들을 몰아넣습니다.
주의할 점: LoadBalancer 타입을 지원하지 않는 환경(VirtualBox, 홈 랩 등)에서는 EXTERNAL-IP가 계속 <pending> 상태로 남습니다. 클라우드 컨트롤러가 없으니 프로비저닝해줄 주체가 없는 거죠. 이 경우 동작은 NodePort처럼 떨어집니다.
베어메탈에서 LoadBalancer 기능을 흉내내고 싶다면 MetalLB를 깔면 됩니다.
어떤 타입을 언제 쓸까
판단 기준은 의외로 단순합니다.
- 클러스터 내부에서만 호출되는 서비스 → ClusterIP
- 외부 접근 필요 + 클라우드 환경 → LoadBalancer (보통 Ingress와 함께)
- 외부 접근 필요 + 베어메탈/데모 → NodePort (또는 MetalLB)
| 타입 | 용도 | 외부 접근 | 프로덕션 적합성 |
|---|---|---|---|
| ClusterIP | 내부 서비스 | X | ⭐⭐⭐ |
| NodePort | 개발/데모 | O | ⭐ |
| LoadBalancer | 공개 서비스 | O | ⭐⭐⭐ |
결국 "이 서비스를 누가 부르는가"만 분명히 하면 타입은 거의 자동으로 정해집니다. 외부에서 안 부르는데 외부 노출 타입을 쓰는 건, 잠그면 되는 문을 활짝 열어두는 셈이고요.
자주 만나는 문제들
현장에서 반복적으로 마주치는 패턴들입니다. 증상만 보고 원인을 좁히는 데 도움이 됩니다.
EXTERNAL-IP가 계속 <pending>: 클라우드 컨트롤러 없는 환경에서 LoadBalancer 타입을 쓴 경우. NodePort로 바꾸거나 MetalLB 설치.
NodePort로 "Connection refused": 셀렉터가 파드와 매칭되지 않는 경우가 많습니다. kubectl get endpoints <svc>로 ENDPOINTS가 비어 있는지 먼저 확인.
DNS는 되는데 curl이 안 됨: 매칭되는 파드는 있는데 Ready 상태가 아닌 경우. readinessProbe 설정을 점검.
노드 간 파드 통신 실패: 십중팔구 CNI 문제. kubectl get pods -n kube-system에서 CNI 파드들이 Running인지 확인.
공통점이 하나 보입니다. 대부분의 네트워킹 장애는 "어느 계층에서 끊겼는지"만 정확히 짚으면 절반은 풀린다는 점이요. 서비스인지, 셀렉터인지, 파드인지, CNI인지. 무작정 yaml을 뜯어고치기 전에 계층부터 가르는 게 빠릅니다.
정리하며
쿠버네티스 네트워킹은 처음엔 한 덩어리로 보여서 복잡하게 느껴집니다. 하지만 뜯어보면 파드 네트워킹 + 서비스 추상화, 이 두 축으로 요약됩니다. CNI가 파드 간 통신을 책임지고, 서비스가 안 변하는 엔드포인트를 제공하는 구조죠. 변하는 것(파드)과 안 변해야 하는 것(서비스)을 분리해놓은 설계라고 보면 머릿속이 편해집니다.
내부 통신엔 ClusterIP, 외부 노출엔 상황에 따라 NodePort나 LoadBalancer. 이 기본기만 손에 익혀도 일상에서 만나는 네트워킹 문제는 대부분 가닥이 잡힙니다.
생각해보니 몇 년 전 나를 한참 붙잡았던 그 시간 초과 에러도, 따지고 보면 서비스를 제대로 안 만들어놓고 파드에 직접 붙으려 했던 게 전부였더라고요. 계층을 몰랐을 뿐이었던 거죠.