Kubernetes 정복기: 네트워킹 심화와 첫 애플리케이션 배포 (Day 2)

문한성·2025년 10월 30일
0

K8S 정복하기

목록 보기
2/9

3노드 클러스터 환경에서 Control Plane부터 실제 애플리케이션 배포까지

들어가며

Day 1에서 클러스터의 기본 구조를 이해했다면, Day 2는 실전이었습니다. Control Plane이 어떻게 동작하는지, Pod 간 통신이 실제로 어떻게 이루어지는지, 그리고 마침내 외부에서 접근 가능한 웹 애플리케이션을 배포하는 것까지 경험했습니다.

특히 이번 실습에서는 예상치 못한 네트워킹 문제를 직접 해결하면서, Calico CNI의 동작 원리와 Linux 네트워킹에 대해 깊이 이해할 수 있었습니다.

학습 환경

  • 클러스터: 3노드 (cpu1: Master+Worker, cpu2/gpu1: Worker)
  • Kubernetes: v1.31.13
  • CNI: Calico (VXLAN CrossSubnet 모드)
  • Pod Network: 10.244.0.0/16
  • Service Network: 10.96.0.0/12

실습 내용

1. Control Plane 심화 검증

etcd: Kubernetes의 두뇌

첫 번째로 etcd가 실제로 무엇을 저장하는지 확인했습니다.

kubectl exec -n kube-system etcd-cpu1 -- sh -c \
  "ETCDCTL_API=3 etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  get /registry/ --prefix --keys-only" | head -20

출력 결과:

/registry/apiregistration.k8s.io/apiservices/v1.
/registry/apiregistration.k8s.io/apiservices/v1.admissionregistration.k8s.io
/registry/clusterrolebindings/calico-kube-controllers
/registry/deployments/calico-system/calico-kube-controllers
/registry/pods/calico-system/calico-node-ftrzj
/registry/services/endpoints/default/kubernetes
...

모든 리소스가 /registry/ 아래에 계층 구조로 저장되어 있었습니다. Deployment, Pod, Service 모두 etcd에 영구 저장되는 것을 직접 확인했습니다.

API Server: REST API 직접 호출

kubectl get --raw /version
{
  "major": "1",
  "minor": "31",
  "gitVersion": "v1.31.13"
}

API Server는 80개의 API 리소스를 제공하고 있었습니다. kubectl이 실제로는 이 REST API를 호출하는 클라이언트에 불과하다는 것을 깨달았습니다.

Controller Manager와 Scheduler 검증

Deployment를 생성하면 Controller Manager가 ReplicaSet을 생성하고, ReplicaSet Controller가 Pod를 생성하고, Scheduler가 노드를 선택하는 전체 체인을 추적했습니다.

kubectl get events --sort-by='.lastTimestamp' | grep test-controller
Successfully assigned default/test-controller-xxx to cpu2
Pulling image "nginx:alpine"
Created container nginx
Started container nginx

이 과정이 1초도 안 걸렸습니다. 각 컴포넌트가 얼마나 빠르게 동작하는지 놀라웠습니다.


2. VXLAN 네트워킹의 진실

가장 큰 오해: "Pod 통신은 Layer 2?"

처음에는 "Pod 통신이 Layer 2라서 UDP로 감싸야 한다"고 완전히 잘못 이해했습니다. 이 오해를 바로잡는 과정이 Day 2의 가장 큰 학습이었습니다.

핵심 질문:

"BGP로 라우터 정보 넣어서 미리 CIDR 땡겨오게 설정해두면 VXLAN을 사용하지 않아도 되는 것처럼 설명했는데 그게 맞아? Layer 2라서 UDP로 감아야 된다며?"

답변:

Pod 통신은 원래 Layer 3 (IP 기반)입니다!

Pod는 각자 고유한 IP 주소를 가지고 있으며, IP 패킷으로 통신합니다. VXLAN은 "필수"가 아니라 "특정 상황에서의 해결책"입니다.

Calico의 3가지 네트워킹 모드 비교

1. BGP Mode (VXLAN 없음)
[Pod A: 10.244.1.10]
    ↓ (IP routing)
[Node1: 172.30.1.43]
    ↓ (BGP 라우팅 정보 교환)
[Node2: 172.30.1.80]
    ↓ (IP routing)
[Pod B: 10.244.2.20]
  • 조건: 노드 간 L3 라우팅 가능 (물리 라우터가 BGP 지원)
  • 장점: VXLAN overhead 없음, 성능 최고
  • 단점: 물리 네트워크가 BGP를 지원해야 함
  • MTU: 1500 (overhead 없음)
2. VXLAN Mode (순수 overlay)
[Pod A: 10.244.1.10]
    ↓ (IP packet)
[VXLAN 캡슐화: UDP 4789]
    ↓ (Outer IP: 172.30.1.43 → 172.30.1.80)
[물리 네트워크]
    ↓
[VXLAN 역캡슐화]
    ↓
[Pod B: 10.244.2.20]
  • 조건: BGP 불가능 (클라우드 VPC, 제한된 네트워크)
  • 장점: 물리 네트워크와 무관하게 동작
  • 단점: 50 bytes overhead, 성능 저하
  • MTU: 1450
3. VXLAN CrossSubnet Mode (우리 클러스터!)
같은 서브넷:
[Pod A] → [Direct IP routing] → [Pod B]  (MTU 1500)

다른 서브넷:
[Pod A] → [VXLAN tunnel] → [Pod B]  (MTU 1450)
  • 조건: 일부 노드는 같은 서브넷, 일부는 다른 서브넷
  • 장점: 최적의 성능 (같은 서브넷) + 유연성 (다른 서브넷)
  • 우리 환경: 모든 노드가 172.30.1.0/24 → 실제로는 Direct routing만 사용!

VXLAN은 언제 필요한가?

VXLAN이 필요한 경우:
1. 클라우드 환경 (AWS VPC, GCP, Azure)에서 BGP 불가
2. 물리 라우터가 BGP를 지원하지 않음
3. 보안 정책으로 BGP peer 설정 불가
4. 서로 다른 데이터센터/서브넷을 연결

VXLAN이 불필요한 경우:
1. 물리 라우터가 BGP 지원 (Calico BGP mode 사용)
2. 같은 L2 네트워크 내 (Calico CrossSubnet의 direct routing)
3. 노드가 적고 static route로 충분

우리 클러스터는?

  • 모든 노드: 172.30.1.0/24 (같은 서브넷)
  • Calico 모드: VXLAN CrossSubnet
  • 실제 동작: Direct IP routing (VXLAN 미사용)
  • VXLAN 인터페이스: 존재하지만 대기 상태

VXLAN 실제 구성 확인

ip -d link show type vxlan
20: vxlan.calico: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450
    vxlan id 4096 local 172.30.1.43 dev enp2s0 srcport 0 0 dstport 4789
  • VNI: 4096 (Virtual Network Identifier)
  • UDP Port: 4789 (VXLAN 표준 포트)
  • MTU: 1450 = 1500(물리) - 50(VXLAN overhead)
  • 상태: 인터페이스는 존재하지만 실제로는 사용 안 됨

MTU가 1450인 이유

VXLAN을 사용할 때의 헤더 구조:

[Outer Ethernet: 14 bytes]
[Outer IP: 20 bytes]
[UDP: 8 bytes]
[VXLAN: 8 bytes]
[Inner Ethernet: 14 bytes]
[Inner IP: 20 bytes]
[Payload]

Total Overhead: 14+20+8+8 = 50 bytes

물리 인터페이스 MTU가 1500이므로:

  • VXLAN MTU: 1500 - 50 = 1450
  • Fragmentation 방지

우리 클러스터는 Direct routing을 사용하므로 실제로는 overhead가 없지만, VXLAN으로 전환 시를 대비해 1450으로 설정되어 있습니다.

CrossSubnet 모드 동작 확인

우리 클러스터는 모든 노드가 172.30.1.0/24 서브넷에 있어서, VXLAN 없이 직접 라우팅을 사용합니다:

ip route | grep 10.244
10.244.5.192/26 via 172.30.1.38 dev enp2s0 proto 80 onlink     # gpu1으로 가는 경로
10.244.102.128/26 via 172.30.1.80 dev enp2s0 proto 80 onlink   # cpu2로 가는 경로

해석:

  • via 172.30.1.38: gpu1 노드 IP를 next-hop으로 사용
  • dev enp2s0: 물리 Ethernet 인터페이스 사용 (VXLAN 터널 아님!)
  • proto 80: Calico가 설정한 라우팅 (BIRD BGP)
  • onlink: Gateway가 직접 연결된 링크에 있음

확인 방법: VXLAN이 실제로 사용되는지?

# VXLAN 인터페이스의 트래픽 카운터 확인
ip -s link show vxlan.calico
20: vxlan.calico: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450
    RX: bytes  packets  errors  dropped
        0       0        0       0      # ← 수신 패킷 0
    TX: bytes  packets  errors  dropped
        0       0        0       0      # ← 송신 패킷 0

결과: VXLAN 인터페이스를 통한 트래픽이 전혀 없음! Direct routing만 사용되고 있습니다.

Layer 2 vs Layer 3: 최종 정리

오해:

  • "Pod끼리 통신하려면 Layer 2 연결이 필요하다"
  • "그래서 VXLAN으로 Layer 2를 overlay해야 한다"

진실:

  • Pod 통신은 순수 Layer 3 (IP routing)
  • Pod는 서로 다른 IP 서브넷에 있어도 됨
  • 각 노드의 Calico가 IP 라우팅 정보 교환 (BGP)
  • VXLAN은 BGP가 불가능할 때의 "우회 방법"

비유:

  • BGP Mode: 도시 간 고속도로 (직접 연결, 빠름)
  • VXLAN Mode: 지하 터널 (우회, 느리지만 어디든 갈 수 있음)
  • CrossSubnet: 같은 도시는 고속도로, 다른 도시는 터널

3. Pod 네트워킹 구조

veth pair 이해하기 (그리고 헷갈렸던 인터페이스 번호)

각 Pod는 완전히 격리된 network namespace를 가지며, veth pair라는 가상 이더넷 케이블로 호스트와 연결됩니다.

Pod 내부에서 확인:

kubectl exec nettest -- ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536
    inet 127.0.0.1/8 scope host lo

2: eth0@if22: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450
    inet 10.244.102.137/32 scope global eth0

호스트에서 확인:

ip link show | grep cali
22: calie107cf6613e@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue
    link-netns cni-2594a3d8-7097-90e6-9db2-a3aaef8edf77

인터페이스 번호의 혼동

처음에는 이렇게 이해했습니다 (잘못됨):

  • Pod의 eth0@if22 → "if22는 22번 인터페이스를 의미하니까... 호스트도 22번이겠지?"
  • 호스트의 22: cali...@if2 → "22번 인터페이스 맞네! 근데 @if2는 뭐지?"

이건 완전히 틀렸습니다!

올바른 이해:

  • Pod 입장: 2: eth0@if22

    • 내 인터페이스 번호는 2번 (eth0)
    • 상대방(호스트)의 인터페이스 번호는 22번 (@if22)
  • Host 입장: 22: calie107cf6613e@if2

    • 내 인터페이스 번호는 22번 (calie107cf6613e)
    • 상대방(Pod)의 인터페이스 번호는 2번 (@if2)

@ifN의 진정한 의미: "저 너머 네임스페이스에 있는 상대방의 인터페이스 번호"

veth pair 시각화

┌─────────────────────────────────┐       ┌──────────────────────────┐
│      Pod nettest Namespace   │       │     Host Namespace       │
│                              │       │                          │
│  1: lo (127.0.0.1)           │       │  1: lo                   │
│  2: eth0@if22 ←────────────────┼───────┼─→ 22: cali...@if2        │
│     10.244.102.137/32        │       │     (no IP)              │
│                              │       │                          │
│     ↓                        │       │     ↓                    │
│  default via 169.254.1.1     │       │  10.244.102.137 dev cali │
│                              │       │  (proxy ARP)             │
└─────────────────────────────────┘       └──────────────────────────┘

veth pair 특징:
1. 항상 쌍으로 존재 (한쪽만 있을 수 없음)
2. 한쪽 끝이 다른 네임스페이스에 있음
3. 호스트 측 인터페이스는 IP가 없음 (Layer 2 bridge 역할)
4. Pod 측 인터페이스는 Pod IP 할당

실제 통신 흐름

Pod에서 외부로 패킷을 보낼 때:

1. Pod nettest (10.244.102.137)
   → "10.109.60.89로 가고 싶어!"

2. Pod의 라우팅 테이블 확인
   → default via 169.254.1.1 dev eth0
   → eth0@if22로 전송

3. veth pair 통과
   → 호스트의 22번 인터페이스 (calie107cf6613e)로 도착

4. 호스트 라우팅 테이블 확인
   → 10.109.60.89는 Service IP → iptables 규칙 적용
   → DNAT: 10.109.60.89 → 10.244.184.84 (실제 Pod IP)

5. 호스트 라우팅 다시 확인
   → 10.244.184.84는 cpu1 노드에 있음
   → 같은 노드이므로 다른 veth pair로 전달

6. 목적지 Pod의 veth pair 통과
   → nginx Pod에 도달!

중요한 발견: @ifN 표기는 반대편 네임스페이스의 인터페이스 번호입니다!

Pod 라우팅

kubectl exec nettest -- ip route
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0 scope link

Calico는 모든 Pod에 동일한 gateway (169.254.1.1)를 할당하지만, 실제 라우팅은 호스트의 veth pair에서 처리합니다.


4. Service와 Endpoints: 로드밸런싱의 비밀

ClusterIP Service 생성

kubectl create deployment nginx-test --image=nginx:alpine --replicas=3
kubectl expose deployment nginx-test --port=80 --type=ClusterIP
NAME         TYPE        CLUSTER-IP     PORT(S)
nginx-test   ClusterIP   10.109.60.89   80/TCP

Endpoints 자동 관리

kubectl get endpoints nginx-test -o yaml
subsets:
- addresses:
  - ip: 10.244.102.138
    nodeName: cpu2
  - ip: 10.244.184.84
    nodeName: cpu1
  - ip: 10.244.5.203
    nodeName: gpu1
  ports:
  - port: 80

Service의 selector (app=nginx-test)와 일치하는 모든 Pod IP가 자동으로 Endpoints에 추가됩니다!

kube-proxy의 iptables 마법

iptables-save | grep nginx-test
-A KUBE-SERVICES -d 10.109.60.89/32 ... -j KUBE-SVC-W67AXLFK7VEUVN6G
-A KUBE-SVC-W67AXLFK7VEUVN6G ... --probability 0.33333 -j KUBE-SEP-SOT6P6LQ532M4OEI
-A KUBE-SVC-W67AXLFK7VEUVN6G ... --probability 0.50000 -j KUBE-SEP-3DGSCWRDZYIA2HSW
-A KUBE-SEP-SOT6P6LQ532M4OEI ... -j DNAT --to-destination 10.244.102.138:80
-A KUBE-SEP-3DGSCWRDZYIA2HSW ... -j DNAT --to-destination 10.244.184.84:80

동작 원리:
1. Service IP (10.109.60.89)로 들어오는 트래픽을 캡처
2. 확률적으로 분산 (33%, 50%, 나머지 17%)
3. DNAT로 실제 Pod IP:Port로 변환

statistic random 모듈을 사용한 간단하지만 효과적인 로드밸런싱입니다!


5. 첫 웹 애플리케이션 배포

ConfigMap으로 설정 분리

kubectl create configmap webapp-config --from-literal=index.html='
<!DOCTYPE html>
<html>
<head>
    <title>Kubernetes Web App</title>
</head>
<body>
    <h1>Welcome to Kubernetes!</h1>
    <p>This page is served from a ConfigMap</p>
</body>
</html>
'

Deployment에 ConfigMap 마운트

apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webapp
  template:
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        volumeMounts:
        - name: html-volume
          mountPath: /usr/share/nginx/html
      volumes:
      - name: html-volume
        configMap:
          name: webapp-config

ConfigMap이 Volume으로 마운트되어 nginx가 해당 HTML을 서비스합니다!

NodePort로 외부 노출

kubectl expose deployment webapp --type=NodePort --port=80
NAME     TYPE       CLUSTER-IP      PORT(S)
webapp   NodePort   10.103.193.83   80:32065/TCP

모든 노드의 32065 포트로 접근 가능합니다:

curl http://172.30.1.43:32065
<h1>Welcome to Kubernetes!</h1>
<p>This page is served from a ConfigMap</p>

성공! 처음으로 외부에서 접근 가능한 웹 애플리케이션을 배포했습니다.


6. Volume: 컨테이너 간 데이터 공유

EmptyDir 실습

apiVersion: v1
kind: Pod
metadata:
  name: emptydir-test
spec:
  containers:
  - name: writer
    image: busybox:1.28
    command: ['sh', '-c', 'echo "Hello from EmptyDir" > /data/message.txt && sleep 3600']
    volumeMounts:
    - name: shared-data
      mountPath: /data
  - name: reader
    image: busybox:1.28
    command: ['sh', '-c', 'sleep 10 && cat /data/message.txt && sleep 3600']
    volumeMounts:
    - name: shared-data
      mountPath: /data
  volumes:
  - name: shared-data
    emptyDir: {}
kubectl logs emptydir-test -c reader
Hello from EmptyDir

writer 컨테이너가 작성한 파일을 reader 컨테이너가 성공적으로 읽었습니다. EmptyDir는 같은 Pod 내 컨테이너 간 데이터 공유에 완벽합니다!


배운 점

1. Pod 통신은 Layer 3이다 (가장 큰 깨달음!)

가장 큰 오해를 바로잡았습니다.

처음에는 "Pod 통신은 Layer 2이고, 물리적으로 연결되지 않은 노드 간 통신을 위해 VXLAN으로 Layer 2를 터널링해야 한다"고 잘못 생각했습니다.

실제로는:

  • Pod 통신은 순수 Layer 3 (IP routing) 기반
  • 각 Pod는 고유한 IP 주소를 가지며, 서로 다른 서브넷에 있어도 라우팅으로 통신 가능
  • VXLAN은 BGP 라우팅이 불가능할 때의 대안일 뿐

실전 적용:

  • BGP 가능한 환경: Calico BGP Mode 사용 (최고 성능)
  • 클라우드/제한된 네트워크: VXLAN Mode
  • 하이브리드 환경: CrossSubnet Mode (우리 클러스터)

우리 클러스터는 모든 노드가 같은 서브넷(172.30.1.0/24)에 있어서 VXLAN을 전혀 사용하지 않고 enp2s0 물리 인터페이스로 직접 라우팅합니다. ip -s link show vxlan.calico로 확인하면 패킷이 0개!

이 이해를 바탕으로 이제 네트워크 문제가 생기면:
1. 먼저 라우팅 테이블 확인 (ip route | grep 10.244)
2. Calico가 올바른 인터페이스 사용 중인지 확인 (IP_AUTODETECTION_METHOD)
3. BIRD BGP 테이블과 kernel 라우팅 테이블 비교

2. @ifN 표기의 진정한 의미

veth pair를 이해하는 데 한참 헤맸습니다.

  • Pod: 2: eth0@if22 → "내 인터페이스는 2번, 상대방은 22번"
  • Host: 22: cali...@if2 → "내 인터페이스는 22번, 상대방은 2번"

@ifN은 "상대편 네임스페이스의 인터페이스 번호"

이 개념을 이해하니 Pod 네트워킹 디버깅이 훨씬 쉬워졌습니다. veth pair의 한쪽 끝을 찾으면 반대편도 바로 찾을 수 있습니다.

3. VXLAN은 "선택"이지 "필수"가 아니다

VXLAN의 역할 재정의:

  • ❌ "Pod 통신은 항상 VXLAN을 통해 이루어진다"
  • ✅ "VXLAN은 BGP 라우팅이 불가능할 때 사용하는 우회 방법"

성능 고려사항:

  • Direct routing: MTU 1500, overhead 없음, 최고 성능
  • VXLAN: MTU 1450, 50 bytes overhead, 약간의 성능 저하

프로덕션 환경 선택 가이드:

  • 온프레미스 + BGP 가능 → Calico BGP Mode
  • AWS/GCP/Azure → VXLAN Mode (또는 Cloud provider CNI)
  • 멀티 데이터센터 → VXLAN CrossSubnet Mode

4. Kubernetes는 이벤트 기반 시스템

Control Plane의 각 컴포넌트는:

  • API Server를 watch
  • 변경사항 감지 시 즉시 반응
  • 선언적 상태(desired state)를 실제 상태(current state)로 수렴

이 모델이 Kubernetes의 자동화와 자가 치유를 가능하게 합니다.

Deployment 하나 생성하면:
1. API Server → etcd 저장
2. Deployment Controller → ReplicaSet 생성 감지
3. ReplicaSet Controller → Pod 생성 요청
4. Scheduler → 노드 선택
5. kubelet → 컨테이너 실행

전체 과정이 1초 미만! 각 컴포넌트가 독립적으로 자기 역할만 수행하는 아름다운 설계입니다.

5. Service는 단순히 iptables 규칙이다

고급 로드밸런서가 있는 줄 알았는데, kube-proxy가 생성한 iptables 규칙만으로 구현되어 있었습니다.

-A KUBE-SVC-xxx ... --probability 0.33333 -j KUBE-SEP-A
-A KUBE-SVC-xxx ... --probability 0.50000 -j KUBE-SEP-B
-A KUBE-SVC-xxx ... -j KUBE-SEP-C
-A KUBE-SEP-A ... -j DNAT --to-destination 10.244.102.138:80

statistic random 모듈로 확률적 분산. 간단하지만 효과적입니다!

이제 Service 트래픽이 어디로 가는지 추적할 수 있습니다:

iptables-save | grep <service-name>

6. ConfigMap의 강력함

코드와 설정을 완전히 분리할 수 있습니다. 같은 이미지로 dev/staging/prod 환경을 다르게 구성할 수 있습니다.

더 나아가:

  • Volume으로 마운트 → 파일 형태로 사용
  • 환경변수로 주입 → 애플리케이션 설정
  • ConfigMap 변경 → Pod 재시작 없이 반영 (Volume 마운트 시)

7. Network Namespace와 Linux 네트워킹의 힘

각 Pod는 완전히 격리된 네트워크 환경을 가지며, veth pair가 호스트와의 유일한 연결 고리입니다.

Linux 네트워킹 스택의 놀라운 활용:

  • Network Namespace: Pod 격리
  • veth pair: 네임스페이스 간 연결
  • Routing: Layer 3 통신
  • iptables: Service 로드밸런싱, SNAT/DNAT
  • VXLAN: 필요 시 overlay network

Kubernetes는 새로운 기술을 발명한 게 아니라, 기존 Linux 커널 기능을 정교하게 조합한 것입니다. 이 점이 매우 인상적이었습니다.


삽질 포인트

대형 사고: 크로스 노드 Pod 통신 실패

증상

같은 노드의 Pod끼리는 통신이 되는데, 다른 노드의 Pod와는 통신이 안 되는 현상 발생:

kubectl exec nettest -- ping 10.244.184.84
--- 10.244.184.84 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss

원인 발견 과정

  1. nginx가 정상 동작하는지 확인

    kubectl exec nginx-pod -- netstat -tlnp

    → 80 포트 정상 listening ✅

  2. 노드 간 연결 확인

    ping 172.30.1.80  # cpu2

    → 노드 간 ping 정상 ✅

  3. 라우팅 테이블 확인

    ip route | grep 10.244

    cpu2, gpu1로 가는 라우팅 규칙이 없음!

  4. Calico 로그 확인

    kubectl logs -n calico-system calico-node-vd4ls --tail=50
    Interface down, will retry if it goes up. ifaceName="wlp3s0"

범인 발견! Calico가 WiFi 인터페이스 (wlp3s0)를 사용하려다 실패하고 있었습니다.

해결 방법

Calico가 Ethernet 인터페이스 (enp2s0)를 사용하도록 설정:

kubectl set env daemonset/calico-node -n calico-system \
  IP_AUTODETECTION_METHOD=interface=enp.*

cpu1과 gpu1의 calico-node Pod를 재시작:

kubectl delete pod -n calico-system calico-node-vd4ls  # cpu1
kubectl delete pod -n calico-system calico-node-wnwwt  # gpu1

재시작 후 라우팅 확인:

ip route | grep 10.244
10.244.5.192/26 via 172.30.1.38 dev enp2s0 proto 80 onlink     # gpu1 ✅
10.244.102.128/26 via 172.30.1.80 dev enp2s0 proto 80 onlink   # cpu2 ✅

성공! enp2s0 인터페이스를 통해 라우팅되고 있습니다.

교훈

  1. IP_AUTODETECTION_METHOD=first-found는 위험하다

    • 노드에 여러 네트워크 인터페이스가 있으면 예상치 못한 인터페이스 선택 가능
    • 명시적으로 interface=enp.* 또는 can-reach=<IP> 사용 권장
  2. Calico 로그는 디버깅의 보물창고

    • Felix가 라우팅을 추가하지 못하는 이유가 명확히 나옴
    • kubectl logs -n calico-system calico-node-xxx는 필수
  3. BIRD BGP 테이블과 kernel 라우팅 테이블은 다르다

    • BIRD는 올바른 라우팅 정보를 가지고 있었지만
    • Felix가 kernel에 추가하지 못했음
    • kubectl exec -n calico-system calico-node-xxx -- birdcl show route로 확인 가능
  4. DaemonSet 업데이트가 모든 Pod를 재시작하지 않을 수 있다

    • env 변경 후 수동으로 Pod 삭제 필요했음
    • kubectl rollout status로 확인

기타 발견 사항

cpu2 노드 네트워크 인터페이스 변경

WiFi에서 유선(Ethernet)으로 인터넷 연결을 변경하면서 네트워크 인터페이스가 바뀌었습니다.

변경 내역:

  • 인터페이스: wlp3s0 (WiFi) → enp2s0 (Ethernet)
  • IP: 172.30.1.34 → 172.30.1.80 (나중에 같은 IP로 맞춤)

IP는 동일하게 설정했지만, 네트워크 인터페이스가 달라지면서 Calico가 올바른 인터페이스를 찾지 못하는 문제가 발생했습니다. 이것이 바로 IP_AUTODETECTION_METHOD=first-found의 위험성입니다.

교훈:

  • 네트워크 연결 방식을 변경할 때는 Calico 설정도 함께 확인
  • IP_AUTODETECTION_METHOD를 명시적으로 설정 (interface=enp.*)
  • Static IP 설정으로 일관성 유지

veth pair 인터페이스 번호 혼동

처음에는 Pod의 eth0@if22가 호스트의 22번 인터페이스를 의미하는 줄 알았으나, 실제로는:

  • Pod: eth0@if22 → 호스트의 22번 인터페이스와 연결됨
  • Host: 22: cali...@if2 → Pod의 2번 인터페이스(eth0)와 연결됨

@ifN상대방 쪽의 인터페이스 번호입니다!


다음 계획 (Day 3)

Day 2에서 기본적인 애플리케이션 배포까지 완료했으니, Day 3에서는 운영 시나리오를 다룰 계획입니다:

1. Secret과 민감 정보 관리

  • ConfigMap vs Secret 차이
  • Secret을 환경변수 및 Volume으로 마운트
  • base64 인코딩의 한계와 주의사항

2. Rolling Update와 무중단 배포

  • Deployment의 롤링 업데이트 전략
  • maxSurge와 maxUnavailable 설정
  • rollout 상태 확인 및 rollback

3. Persistent Volume (PV/PVC)

  • HostPath를 사용한 영구 저장소
  • PV/PVC 라이프사이클
  • StorageClass 이해

4. Resource Limits과 HPA

  • CPU/Memory requests와 limits
  • Pod QoS 클래스 (Guaranteed, Burstable, BestEffort)
  • Horizontal Pod Autoscaler 설정

5. Health Check (Liveness/Readiness Probe)

  • Liveness Probe: 컨테이너 재시작
  • Readiness Probe: Service Endpoints 제어
  • Startup Probe: 느린 시작 애플리케이션 처리

마무리

Day 2는 Day 1보다 훨씬 실전적이었습니다. Control Plane의 동작 원리를 깊이 이해하고, 네트워킹 문제를 직접 해결하면서 Calico와 Linux 네트워킹에 대한 자신감이 생겼습니다.

특히 "Service IP로 접근이 안 된다"는 문제를 만났을 때, 체계적으로 디버깅하여 원인을 찾고 해결한 경험이 가장 값졌습니다. 이제 클러스터에 문제가 생겨도 당황하지 않고 로그와 상태를 확인하며 접근할 수 있을 것 같습니다.

Day 3에서는 실제 프로덕션 환경에서 필요한 운영 기능들을 다뤄볼 예정입니다. Rolling Update, Health Check, Resource Limits 등 안정적인 서비스 운영을 위한 필수 요소들을 학습하겠습니다!

profile
기록하고 공유하려고 노력하는 DevOps 엔지니어

0개의 댓글