들어가기전 읽어보기 좋은 개념 : 01_OSI 7계층 모델,02_DNS
🎯 핵심 3가지 네트워킹 요구사항
쿠버네티스는 어떻게 구현할지는 자유, 하지만 이 3가지는 반드시 지켜라!
모든 파드는 고유 IP를 가진다
전통적 Docker 방식 (문제):
Host IP: 192.168.1.100
├─ Container1 → 192.168.1.100:8080 (포트 매핑)
└─ Container2 → 192.168.1.100:8081 (포트 충돌 관리 필요)
❌ 문제점:
- 포트 충돌 관리 복잡
- 컨테이너가 외부 노출 IP/포트를 알 수 없음
- 동적 스케일링 어려움
쿠버네티스 방식 (해결):
Node IP: 192.168.1.100
├─ Pod1: 10.244.1.5 (독립된 IP!)
└─ Pod2: 10.244.1.6 (독립된 IP!)
✅ 장점:
- 포트 충돌 없음 (각자 80포트 사용 가능)
- VM처럼 독립적인 네트워크 환경
- 개발자는 포트 매핑 신경 안 써도 됨
CNI가 이를 구현하는 방법
파드 생성 플로우:
1. 사용자: kubectl run nginx --image=nginx
↓
2. 스케줄러: "이 파드는 node-1로!"
↓
3. node-1의 kubelet: "새 파드 할당됨 감지"
↓
4. kubelet → containerd:
"컨테이너 생성하고 network namespace 만들어줘"
(네트워크 네임스페이스 = 격리된 네트워크 공간)
├─ 독립된 IP 주소
├─ 독립된 라우팅 테이블
└─ 독립된 방화벽 규칙
↓
5. kubelet → CNI 플러그인 호출:
/opt/cni/bin/calico ADD \
--containerid=abc123 \
--netns=/var/run/netns/pod-abc123
↓
6. CNI 플러그인 작업:
a) IPAM(IP 주소 관리) 호출
"이 노드의 파드 대역: 10.244.1.0/24"
→ 사용 가능한 IP: 10.244.1.5 할당!
b) veth pair 생성 (가상 케이블)
┌─────────────────────────────┐
│ Pod Namespace │ Node Root Namespace
│ │
│ eth0 (10.244.1.5) ◄─────► vethXXX ─┐
└─────────────────────────────┘ │
│
┌────▼────┐
│ cni0 │ (브릿지)
│ bridge │
└─────────┘
c) IP/Gateway 설정
Pod 내부 eth0:
- IP: 10.244.1.5/24
- Gateway: 10.244.1.1 (cni0 브릿지)
d) 라우팅 규칙 추가
Node의 라우팅 테이블:
10.244.1.5 → vethXXX (이 파드로!)
↓
7. kubelet: "성공!" → 파드 Running 상태
2️⃣ 모든 파드는 NAT 없이 서로 통신 가능
NAT가 뭔가요?
집 (사설 IP: 192.168.0.10) → 공유기 (공인 IP: 203.0.113.5)
출발지 IP를 바꿔서 전송하는 것!
외부에서 보면: "어? 203.0.113.5에서 온 패킷이네"
실제 출발지: 192.168.0.10 (숨겨짐!)
쿠버네티스는 왜 NAT 없는 통신을 요구하나?
NAT가 있으면 (문제):
Pod A (10.244.1.5) → NAT → Pod B
↓
Pod B가 받은 패킷:
출발지: 192.168.1.100 (Node IP로 변환됨)
❌ 문제:
- 실제 출발지(Pod A)를 알 수 없음
- 로그에 Node IP만 찍힘
- 보안 정책 적용 불가 (어떤 파드인지 모름)
- 디버깅 어려움
NAT 없는 통신 (쿠버네티스):
Pod A (10.244.1.5) → 직접 → Pod B (10.244.2.10)
Pod B가 받은 패킷:
출발지: 10.244.1.5 (Pod A의 실제 IP)
✅ 장점:
- 투명한 통신 (출발지 명확)
- 로그/모니터링 정확
- 보안 정책 적용 가능
- 서비스 메시 구현 용이
같은 노드 vs 다른 노드 통신
🏠 Case 1: 같은 노드 내 파드 통신 (L2)
node-1:
Pod A (10.244.1.5) → cni0 bridge → Pod B (10.244.1.6)
단순히 브릿지만 거침! (매우 빠름)
🌐 Case 2: 다른 노드의 파드 통신 (L3)
방법 1️⃣: Overlay Network (Flannel VXLAN)
Pod A Pod B
10.244.1.5 10.244.2.10
(node-1) (node-2)
↓
1. Pod A: "10.244.2.10으로 보낼게"
↓
2. node-1의 flanneld:
"어? 다른 노드 파드네. 캡슐화 필요!"
[원본 패킷]
Src: 10.244.1.5
Dst: 10.244.2.10
↓ VXLAN 캡슐화
[VXLAN 헤더 + 원본 패킷]
Outer Src: 192.168.1.100 (node-1 IP)
Outer Dst: 192.168.1.101 (node-2 IP)
3. 물리 네트워크를 통해 node-2로 전송
↓
4. node-2의 flanneld:
"VXLAN 패킷이네. 캡슐 벗겨야지"
↓ 역캡슐화
[원본 패킷]
Src: 10.244.1.5
Dst: 10.244.2.10
5. Pod B에 전달!
💡 Pod B 입장: NAT 없이 Pod A의 진짜 IP 보임!
방법 2️⃣: Direct Routing (Calico BGP)
Pod A Pod B
10.244.1.5 10.244.2.10
(node-1) (node-2)
1. node-1: "10.244.2.0/24는 node-2로 보내!"
node-2: "10.244.1.0/24는 node-1로 보내!"
↓ BGP로 서로 경로 광고
2. Pod A → 10.244.2.10으로 패킷 전송
↓
3. node-1 라우팅 테이블:
"10.244.2.0/24 → 192.168.1.101 (node-2)"
4. 직접 라우팅! (캡슐화 없음)
[원본 패킷 그대로]
Src: 10.244.1.5
Dst: 10.244.2.10
5. node-2로 도착 → Pod B에 전달
💡 오버헤드 없어서 더 빠름!
3️⃣ 노드와 파드는 양방향 NAT 없이 통신
왜 필요한가?
1. Node → Pod 통신 필요 상황:
kubelet이 파드 상태 체크:
curl http://10.244.1.5:8080/healthz
Prometheus가 메트릭 수집:
curl http://10.244.1.5:9090/metrics
2. Pod → Node 통신 필요 상황:
파드가 kubelet API 호출:
curl https://192.168.1.100:10250/stats
DaemonSet의 node-exporter 접근:
curl http://192.168.1.100:9100/metrics
실제 동작
노드의 프로세스들:
┌─────────────────────────────────────────┐
│ Node (192.168.1.100) │
│ │
│ kubelet (10250) │
│ node-exporter (9100) │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Pod A │ │ Pod B │ │
│ │ 10.244.1.5 │ │ 10.244.1.6 │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────┘
Node → Pod:
kubelet이 10.244.1.5:8080 호출
→ cni0 브릿지 거쳐 Pod A 도달
→ Pod A는 출발지 192.168.1.100 확인 (Node IP)
Pod → Node:
Pod A가 192.168.1.100:10250 호출
→ 기본 게이트웨이(cni0) 거쳐 Node로
→ kubelet은 출발지 10.244.1.5 확인 (Pod IP)
✅ 양쪽 다 실제 IP 그대로!
📦 4가지 IP 주소 종류
계층별 IP 정리
외부 세계
↓
[External IP] 203.0.113.5 (로드밸런서)
↓
클러스터 경계
↓
[Cluster IP] 10.96.0.10 (Service)
↓ kube-proxy가 로드밸런싱
├─ [Pod IP] 10.244.1.5
├─ [Pod IP] 10.244.1.6
└─ [Pod IP] 10.244.2.10
이 모든 파드들이 실행되는 곳:
[Node IP] 192.168.1.100 (물리/가상 머신)
1️⃣ Pod IP
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx
```
```
생성되면:
$ kubectl get pod nginx -o wide
NAME READY STATUS IP NODE
nginx 1/1 Running 10.244.1.5 node-1
특징:
✅ 클러스터 내부에서만 접근 가능
✅ 파드 재시작 시 IP 변경됨 (비영속적)
✅ CNI가 할당 (10.244.0.0/16 같은 대역)
✅ 파드 내 모든 컨테이너가 공유
실제 통신:
$ kubectl exec pod-a -- curl http://10.244.1.5
→ 가능 (같은 클러스터)
$ curl http://10.244.1.5
→ 불가능 (클러스터 외부에서)
```
### 2️⃣ Node IP
```
물리/가상 머신의 IP:
$ kubectl get nodes -o wide
NAME STATUS INTERNAL-IP EXTERNAL-IP
node-1 Ready 192.168.1.100 34.123.45.67
node-2 Ready 192.168.1.101 34.123.45.68
Internal-IP: 사설 네트워크 IP (VPC 내부)
External-IP: 공인 IP (있을 수도, 없을 수도)
특징:
✅ 인프라 레벨 IP
✅ 노드가 살아있는 동안 안정적
✅ kubelet API 접근용3️⃣ Cluster IP (Service IP)
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: ClusterIP # 기본값
selector:
app: nginx
ports:
- port: 80
targetPort: 8080
```
```
생성되면:
$ kubectl get svc my-service
NAME TYPE CLUSTER-IP PORT(S)
my-service ClusterIP 10.96.0.10 80/TCP
특징:
✅ 가상 IP (실제 인터페이스 없음!)
✅ 클러스터 내부에서만 접근 가능
✅ 서비스가 살아있는 동안 불변
✅ kube-proxy가 iptables/IPVS로 구현
동작 원리:
클라이언트 파드: curl http://10.96.0.10:80
↓
kube-proxy의 iptables 규칙:
"10.96.0.10:80 → 랜덤으로 하나 선택"
├─ 10.244.1.5:8080 (33%)
├─ 10.244.1.6:8080 (33%)
└─ 10.244.2.10:8080 (33%)
실제로는 Cluster IP로 가는 게 아니라
바로 Pod IP로 전달됨!4️⃣ External IP
apiVersion: v1
kind: Service
metadata:
name: web-service
spec:
type: LoadBalancer # 외부 노출
selector:
app: web
ports:
- port: 80
targetPort: 8080
```
```
클라우드에서 생성되면:
$ kubectl get svc web-service
NAME TYPE CLUSTER-IP EXTERNAL-IP
web-service LoadBalancer 10.96.0.20 34.123.45.100
특징:
✅ 인터넷에서 접근 가능한 IP
✅ 클라우드 제공자가 로드밸런서 생성
✅ 비용 발생 (AWS ELB, GCP LB 등)
통신 흐름:
인터넷 사용자
↓
External IP: 34.123.45.100:80
↓ (클라우드 로드밸런서)
NodePort: 192.168.1.100:30080
↓ (kube-proxy)
Cluster IP: 10.96.0.20:80
↓ (로드밸런싱)
Pod IP: 10.244.1.5:8080
```
---
## 🔧 CNI 플러그인 실전 비교
### 시나리오별 선택 가이드
```
📚 학습/개발 환경:
→ Flannel (K3s 기본)
- 설정 0개
- 바로 사용 가능
- 네트워크 정책 필요 없음
🏢 프로덕션 (보안 중요):
→ Calico
- 강력한 네트워크 정책
- BGP로 고성능
- 대규모 검증됨
🚀 최신 기술 + 최고 성능:
→ Cilium
- eBPF 기반
- L7 정책 가능
- Hubble로 가시성
☁️ 클라우드 환경:
→ 해당 클라우드 CNI
- AWS VPC CNI
- Azure CNI
- GKE CNI
```
### 실제 동작 차이
```
Pod A (node-1) → Pod B (node-2) 통신 시:
Flannel (VXLAN):
1. 패킷 캡슐화 필요
2. UDP 오버헤드 있음
3. 지연시간: ~10% 증가
4. 네트워크 정책: ❌
Calico (BGP):
1. 직접 라우팅
2. 오버헤드 거의 없음
3. 지연시간: 최소
4. 네트워크 정책: ✅ 강력
Cilium (eBPF):
1. 커널 레벨 처리
2. iptables 우회
3. 지연시간: 최소
4. 네트워크 정책: ✅ L7까지
5. 추가: HTTP 경로 제어 가능!
```
---
## 💡 핵심 정리
### 레이어별 역할
| 레이어 | 컴포넌트 | 역할 |
|--------|---------|------|
| **애플리케이션** | 컨테이너 | 비즈니스 로직 실행 |
| **파드** | Pod (고유 IP) | 컨테이너 그룹 + 네트워크 격리 |
| **서비스** | Service (Cluster IP) | 안정적인 엔드포인트 제공 |
| **노드** | kubelet + CNI | 파드 네트워킹 구현 |
| **물리/가상** | Node (Node IP) | 인프라 레벨 통신 |
### 통신 플로우 전체 그림
```
1. 개발자: kubectl apply -f deployment.yaml
↓
2. 스케줄러: "pod-1은 node-1로, pod-2는 node-2로"
↓
3. 각 노드의 kubelet: CNI 호출
↓
4. CNI: veth pair 생성 + IP 할당
├─ pod-1: 10.244.1.5
└─ pod-2: 10.244.2.10
↓
5. 서비스 생성:
ClusterIP: 10.96.0.10 → {pod-1, pod-2}
↓
6. kube-proxy: iptables 규칙 생성
↓
7. 다른 파드: curl http://my-service
↓ DNS로 Cluster IP 조회
↓ kube-proxy가 Pod IP로 변환
↓ CNI가 라우팅
↓ 목적지 파드 도달!💡 핵심 개념 정리
| 개념 | 설명 |
|---|---|
| CNI | 네트워킹 플러그인 표준 인터페이스 |
| VXLAN | 오버레이 네트워크 (캡슐화) |
| BGP | L3 라우팅 프로토콜 |
| eBPF | 커널 프로그래밍 기술 |
| CoreDNS | 클러스터 내부 DNS 서버 |
| FQDN | 완전한 도메인 이름 |
| Default Allow | 기본 허용 모드 (위험) |
| Default Deny | 기본 거부 모드 (안전) |
01. CNI(Container Network Interface)의 역할과 원리
🎯 핵심 개념
CNI는 쿠버네티스가 네트워킹 책임을 외부 플러그인에 위임하기 위한 표준 명세
동작 플로우
1. 파드 스케줄링
↓
2. Kubelet이 파드 할당 인지
↓
3. 컨테이너 런타임 호출 (containerd)
→ 네트워크 네임스페이스 생성 (격리된 네트워크 공간)
↓
4. CNI 플러그인 호출 ⭐️
(Kubelet이 /opt/cni/bin/calico 등 실행)
→ 네임스페이스 경로, 컨테이너 ID 전달
↓
5. CNI 플러그인의 실제 작업:
├─ IPAM 플러그인으로 IP 주소 할당받음
├─ veth pair 생성 (가상 이더넷 쌍)
│ ├─ 한쪽: 호스트의 루트 네임스페이스
│ └─ 다른쪽: 컨테이너의 네임스페이스
├─ 컨테이너 인터페이스에 IP/게이트웨이 설정
└─ 호스트 라우팅 테이블 업데이트
핵심 포인트
- Kubelet은 세부사항을 모름 - 단지 표준 인터페이스로 호출만 함
- 플러그인 아키텍처 - 다양한 CNI 구현체 선택 가능
- 책임 분리 - 쿠버네티스는 오케스트레이션, CNI는 네트워킹
02. CNI 플러그인 비교 분석
1️⃣ Flannel - 단순함의 미학
핵심 기술: VXLAN 오버레이 네트워크
작동 원리
파드A(node-1) → 파드B(node-2) 통신 시:
1. flanneld가 패킷 가로챔
2. VXLAN 헤더로 캡슐화 (Encapsulation)
3. node-2 IP로 전송
4. node-2의 flanneld가 역캡슐화 (Decapsulation)
5. 목적지 파드에 전달
| 항목 | 평가 |
|---|---|
| ✅ 장점 | • 설정 매우 간단 • 기존 네트워크 변경 불필요 • 학습 곡선 낮음 |
| ❌ 단점 | • 캡슐화 오버헤드 • 네트워크 정책 미지원 (치명적) |
| 🎯 추천 | 개발/테스트 환경, 소규모 클러스터 |
2️⃣ Calico - 보안과 확장성의 강자
핵심 기술: BGP 기반 순수 L3 라우팅
작동 원리
각 노드 = BGP 라우터 역할
node-1: "10.244.1.0/26 대역은 나한테 보내!"
node-2: "10.244.2.0/26 대역은 나한테 보내!"
↓ BGP로 서로 광고
캡슐화 없이 네이티브 라우팅으로 통신
| 항목 | 평가 |
|---|---|
| ✅ 장점 | • 강력한 네트워크 정책 • 오버레이 없어 고성능 • 대규모 검증됨 (수천 노드) • 글로벌 정책, deny 규칙 지원 |
| ❌ 단점 | • BGP 설정 복잡할 수 있음 • 환경에 따라 네트워크팀 협력 필요 |
| 🎯 추천 | 프로덕션 표준, 보안 중요 환경, 대규모 클러스터 |
3️⃣ Cilium - eBPF 기반 차세대 혁신
핵심 기술: eBPF (커널 프로그래밍)
eBPF란?
- 커널 소스 수정 없이 커널 동작을 프로그래밍
- 커널 네트워킹 스택 초기 단계에서 패킷 처리
- iptables/IPVS 우회
혁신적 기능
1. L7(API 수준) 네트워크 정책:
"A 서비스는 B 서비스의 /api/v1/read에만
GET 요청 가능" ← HTTP 경로까지 제어!
2. Hubble 가시성:
서비스 간 모든 통신 실시간 시각화
3. 서비스 메시 기능:
사이드카 없이 eBPF로 L7 라우팅
| 항목 | 평가 |
|---|---|
| ✅ 장점 | • 압도적 성능 • API-aware 보안 (L7) • 뛰어난 관측성 (Hubble) • 내장 서비스 메시 |
| ❌ 단점 | • 최신 커널 필요 (4.9+) • 상대적으로 덜 성숙한 커뮤니티 |
| 🎯 추천 | 최고 성능+보안+가시성, 최신 기술 수용 가능한 조직 |
📊 비교 도표
| 기능 | Flannel | Calico | Cilium |
|---|---|---|---|
| 핵심 기술 | VXLAN | BGP | eBPF |
| 성능 | 낮음 | 높음 | 매우 높음 |
| 네트워크 정책 | ❌ | ✅ 강력 | ✅ L7 |
| 확장성 | 중간 | 높음 | 매우 높음 |
| 관측성 | 기본 | 기본 | 뛰어남 |
| 설정 난이도 | 매우 쉬움 | 중간 | 중간 |
03. 쿠버네티스 DNS와 서비스 디스커버리
🎯 핵심 문제
IP 주소 대신 이름으로 통신하기
- ClusterIP도 변할 수 있음
- IP는 인간 친화적이지 않음
- 서비스 디스커버리 필요
CoreDNS 동작 플로우
1. 서비스 생성
kubectl create service user-service
↓
2. API 서버가 이벤트 발생
↓
3. CoreDNS가 watch하다가 감지
↓
4. 자동으로 DNS 레코드 생성
├─ user-service.production (짧은 이름)
└─ user-service.production.svc.cluster.local (FQDN)
↓
5. 파드의 /etc/resolv.conf에 자동 설정
├─ nameserver: CoreDNS ClusterIP
└─ search: production.svc.cluster.local
DNS 이름 규칙
<service-name>.<namespace>.svc.cluster.local
↓ ↓ ↓ ↓
서비스명 네임스페이스 서비스 클러스터도메인
예시:
- 같은 네임스페이스:
user-service - 다른 네임스페이스:
user-service.production - 완전한 이름:
user-service.production.svc.cluster.local
위치 투명성 (Location Transparency)
개발자 코드:
USER_SERVICE_HOST=user-service
http://{USER_SERVICE_HOST}/api/users
→ CoreDNS가 ClusterIP로 변환
→ 서비스가 실제 파드 IP로 로드밸런싱
→ 파드가 어느 노드에 있든 상관없음!
장점:
- IP 주소 관리 불필요
- 동적 환경에서 안정적
- 마이크로서비스 간 느슨한 결합
04. 네트워크 정책 - 제로 트러스트
🚨 문제: 기본적으로 허용 (Default Allow)
현재 상태: 모든 파드 ↔ 모든 파드 통신 가능
위험:
frontend → database 직접 접속 가능
침해된 파드 → 측면 이동(Lateral Movement) 가능
제로 트러스트 원칙
“Never Trust, Always Verify”
- 내부 네트워크도 믿지 않음
- 모든 통신은 명시적 허가 필요
- 최소 권한의 원칙
네트워크 정책 구조
yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-allow-backend
spec:
podSelector: # 1️⃣ 누구에게 적용?
matchLabels:
role: database
policyTypes: # 2️⃣ 방향
- Ingress # 인바운드 제어
ingress: # 3️⃣ 규칙
- from: # 출발지
- podSelector:
matchLabels:
app: backend
ports: # 포트
- protocol: TCP
port: 6379
```
### 작동 원리
```
정책 적용 전 (Default Allow):
모든 파드 → database:6379 ✅
정책 적용 후 (Default Deny + 명시적 허용):
app: backend → database:6379 ✅
app: frontend → database:* ❌
app: backend → database:3306 ❌
```
### 핵심 포인트
1. **정책 하나라도 적용되면 Default Deny로 전환**
2. **레이블 셀렉터 기반** - 동적으로 파드 선택
3. **CNI가 실제 구현** - iptables, eBPF 등으로 변환
4. **L3/L4 수준 제어** - IP, 포트, 프로토콜
### 선택 기준
- **podSelector**: 파드 레이블로 선택
- **namespaceSelector**: 네임스페이스로 선택
- **ipBlock**: IP 대역으로 선택
---
## 🔄 전체 플로우 요약
```
1. CNI가 파드에 IP 할당
└─ Kubelet → CNI 플러그인 → IP/라우팅 설정
2. 서비스가 안정적인 진입점 제공
└─ ClusterIP로 파드 집합 추상화
3. DNS가 이름 기반 디스커버리 제공
└─ CoreDNS가 서비스명 → ClusterIP 변환
4. 네트워크 정책이 보안 제공
└─ 명시적 허용만 통신 가능 (Zero Trust)작성일: 2025-11-05