티스토리 뷰

시리즈: Oracle Cloud + Tailscale + Kubernetes 완벽 가이드
이전: 2단계: Tailscale 메시 네트워크 구성 | 다음: 4단계: 네트워킹 심화 이해


작성일: 2026년 2월 14일
카테고리: Kubernetes, Networking, Cilium
키워드: Tailscale, Cilium, Native Routing, eBPF, Kubernetes CNI

요약

Tailscale 순정(공식 SaaS) 환경에서 Kubernetes 클러스터를 구축하고 Cilium CNI를 Native Routing 모드로 설치하는 가이드다. 기존 VXLAN 버전(03-setup-kubernetes-cilium.md)과 달리, 각 노드가 자신의 Pod CIDR(/24)을 Tailscale --advertise-routes로 광고하고, Tailscale의 policy routing(table 52)이 크로스 노드 Pod 트래픽을 라우팅한다. VXLAN 캡슐화 오버헤드가 없어 MTU 손실이 줄고, 패킷 경로가 단순해진다.

기존 VXLAN 버전과의 관계: 03-setup-kubernetes-cilium.md는 Headscale 환경에서 --advertise-routes 버그로 인해 VXLAN을 선택했다. Tailscale 순정에서는 해당 제약이 없으므로 Native Routing을 사용한다.

이 단계에서 할 일

  1. Container Runtime (containerd) 설치
  2. Kubernetes 패키지 설치
  3. Tailscale 설치 (--accept-routes) + ACL 설정
  4. 마스터 노드 초기화 + Pod CIDR Route 광고
  5. Cilium CNI 설치 (Native Routing 모드)
  6. 워커 노드 추가 + Pod CIDR Route 광고
  7. 클러스터 검증

Phase 1: Container Runtime 설치

모든 노드에서 실행

1. Containerd 설치

# Docker 저장소 추가 (containerd 포함)
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

# Containerd 설치
sudo dnf install -y containerd.io

# 설정 파일 생성
sudo mkdir -p /etc/containerd
sudo containerd config default | sudo tee /etc/containerd/config.toml

# SystemdCgroup 활성화 (중요!)
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml

# 서비스 시작
sudo systemctl restart containerd
sudo systemctl enable containerd

# 확인
sudo systemctl status containerd

2. crictl 설치 (디버깅 도구)

# crictl 다운로드
VERSION="v1.35.0"
wget https://github.com/kubernetes-sigs/cri-tools/releases/download/$VERSION/crictl-$VERSION-linux-arm64.tar.gz
sudo tar zxvf crictl-$VERSION-linux-arm64.tar.gz -C /usr/bin
rm -f crictl-$VERSION-linux-arm64.tar.gz

# crictl 설정
cat <<EOF | sudo tee /etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
timeout: 2
EOF

# 확인
sudo crictl version

Phase 2: Kubernetes 설치

모든 노드에서 실행

1. Kubernetes 저장소 추가

# Kubernetes 공식 저장소
cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.34/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.34/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl
arch=aarch64
EOF

2. Kubernetes 패키지 설치

# 버전 고정 설치 (1.34.1)
sudo dnf install -y \
  kubelet-1.34.1 \
  kubeadm-1.34.1 \
  kubectl-1.34.1 \
  --disableexcludes=kubernetes

# kubelet 서비스 활성화
sudo systemctl enable --now kubelet

Phase 3: Tailscale 설치 및 사전 설정

모든 노드에서 실행

1. Tailscale 설치

# Tailscale 설치
curl -fsSL https://tailscale.com/install.sh | sh

# 서비스 활성화
sudo systemctl enable --now tailscaled

# Tailscale 연결 (브라우저에서 인증) + 다른 노드의 Route 수락 활성화
sudo tailscale up --accept-routes

# 연결 확인
tailscale ip -4
# 예상: 100.x.x.x (Tailscale CGNAT IP)
tailscale status

Tailscale 네트워크 최적화, MTU 설정 등 상세 내용은 02-setup-tailscale-network.md 참고.

왜 Route 광고가 필요한가?

Native Routing에서는 Pod 간 패킷이 캡슐화 없이 직접 전달된다. 예를 들어 Node A의 Pod(10.244.0.5)가 Node B의 Pod(10.244.1.3)에 패킷을 보내면:

Pod A (10.244.0.5)
  → Node A 커널: "10.244.1.0/24는 tailscale0으로"
    → tailscale0: "10.244.1.0/24는 Node B가 광고한 CIDR"
      → WireGuard 암호화 → Node B
        → Node B 커널: "10.244.1.3은 로컬 Pod"
          → Pod B (10.244.1.3)

이 경로에서 Tailscale은 dst=10.244.1.3 패킷을 Node B로 전달해야 한다. Tailscale이 이 경로를 알려면 각 노드가 자신의 Pod CIDR(노드별 /24) 을 광고해야 한다.

VXLAN과의 차이: VXLAN에서는 패킷을 Node B의 Tailscale IP(100.64.0.2)로 캡슐화하므로 Tailscale이 Pod CIDR을 알 필요가 없었다. Native Routing은 이 캡슐화를 제거하는 대신 Route 광고가 필요하다.

왜 노드별 /24를 광고해야 하나?

모든 노드가 동일한 10.244.0.0/16을 광고하면 Tailscale은 이를 HA(고가용성) Subnet Router로 인식한다. 즉, 하나의 노드를 primary로 선택하고 모든 Pod 트래픽을 그 노드로만 전달한다. Node A의 Pod가 Node B의 Pod에 접근하려 해도 트래픽이 primary 노드(Node A 자신일 수도 있다)로 돌아오는 라우팅 루프가 발생한다.

각 노드가 자신에게 할당된 Pod CIDR만 광고하면 Tailscale이 목적지에 따라 정확한 노드로 라우팅한다:

  • Master가 10.244.0.0/24 광고 → 10.244.0.x 트래픽은 Master로
  • Worker 1이 10.244.1.0/24 광고 → 10.244.1.x 트래픽은 Worker 1로

단, Pod CIDR은 kubeadm이 할당하므로 노드 초기화/Join 이후에만 알 수 있다. 이 Phase에서는 ACL과 --accept-routes만 설정하고, 실제 Route 광고는 Phase 4(마스터), Phase 6(워커)에서 수행한다.

2. Tailscale ACL 설정 (Tailscale Admin Console)

Route 광고를 자동 승인하려면 Tailscale ACL에 autoApprovers를 추가한다. Tailscale Admin Console에서 ACL 파일을 편집:

{
  "autoApprovers": {
    "routes": {
      "10.244.0.0/16": ["tag:k8s-node"],
      "10.96.0.0/12":  ["tag:k8s-node"]
    }
  },
  "tagOwners": {
    "tag:k8s-node": ["autogroup:admin"]
  }
}
CIDR 용도
10.244.0.0/16 Pod 네트워크 전체 범위. 각 노드의 /24 서브넷이 이 범위에 포함됨
10.96.0.0/12 Service 네트워크 (kubeadm --service-cidr)

autoApprovers가 동작하지 않는 경우: 노드에 tag:k8s-node 태그가 정확히 부여되지 않았거나 ACL 충돌이 있으면 자동 승인이 실패한다. 이 경우 Admin Console → 해당 노드 → Subnets → Awaiting Approval 옆 Edit 클릭 → Route 수동 승인. 노드마다 각각 승인해야 한다. 실제로 autoApprovers 설정이 있어도 태그 미적용 등으로 수동 승인이 필요한 경우가 흔하다.

이 시점에서는 Route 광고(--advertise-routes)를 하지 않는다. Pod CIDR은 kubeadm 초기화/Join 이후에 할당되므로, 실제 광고는 Phase 4와 Phase 6에서 수행한다.

Phase 4: 마스터 노드 초기화

마스터 노드에서만 실행

1. kubelet node-ip 설정

# Tailscale IP 확인
TAILSCALE_IP=$(tailscale ip -4)
echo "Control Plane Tailscale IP: $TAILSCALE_IP"

# kubelet이 Tailscale IP 사용하도록 설정
echo "KUBELET_EXTRA_ARGS=--node-ip=$TAILSCALE_IP" | sudo tee /etc/sysconfig/kubelet

2. kubeadm 초기화

# 클러스터 초기화 (kube-proxy 없이)
sudo kubeadm init \
  --apiserver-advertise-address=$TAILSCALE_IP \
  --apiserver-cert-extra-sans=$TAILSCALE_IP \
  --pod-network-cidr=10.244.0.0/16 \
  --service-cidr=10.96.0.0/12 \
  --skip-phases=addon/kube-proxy

# 출력되는 join 명령어를 안전한 곳에 저장!
# kubeadm join 100.64.0.1:6443 --token ... --discovery-token-ca-cert-hash ...

3. kubectl 설정

# kubeconfig 설정
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

# 자동완성 설정
echo 'source <(kubectl completion bash)' >> ~/.bashrc
echo 'alias k=kubectl' >> ~/.bashrc
echo 'complete -o default -F __start_kubectl k' >> ~/.bashrc
source ~/.bashrc

# 노드 확인 (NotReady 상태가 정상 - CNI 설치 전)
kubectl get nodes

4. 마스터 Pod CIDR Route 광고

kubeadm이 마스터에 할당한 Pod CIDR을 확인하고, Tailscale에 광고한다:

# 마스터에 할당된 Pod CIDR 확인
MASTER_NAME=$(hostname)
POD_CIDR=$(kubectl get node $MASTER_NAME -o jsonpath='{.spec.podCIDR}')
echo "Master Pod CIDR: $POD_CIDR"
# 예상: 10.244.0.0/24

# 마스터의 Pod CIDR + Service CIDR을 Tailscale에 광고
sudo tailscale set \
  --advertise-routes=$POD_CIDR,10.96.0.0/12

# 광고 확인
tailscale debug prefs | grep AdvertiseRoutes
# 예상: AdvertiseRoutes:[10.244.0.0/24 10.96.0.0/12]

Route 승인: Admin Console → 해당 노드 → Subnets에서 Route가 Approved 상태인지 확인한다. Awaiting Approval 상태이면 Edit 클릭 후 수동 승인. autoApprovers ACL이 정상이면 자동 승인되지만, 태그 미적용 등으로 수동 승인이 필요한 경우가 흔하다.

5. Control Plane Taint 제거 (선택)

단일 노드 또는 테스트 환경에서 마스터에도 Pod 스케줄링:

kubectl taint nodes --all node-role.kubernetes.io/control-plane-

Phase 5: Cilium CNI 설치

마스터 노드에서 실행

1. Helm 설치

# Helm 설치 스크립트
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh

# 확인
helm version

2. Cilium 설치 준비

# Cilium Helm 차트 추가
helm repo add cilium https://helm.cilium.io/
helm repo update

# Tailscale IP 확인
TAILSCALE_IP=$(tailscale ip -4)
echo "API Server IP: $TAILSCALE_IP"

3. Native Routing vs VXLAN

왜 Native Routing을 선택하나?

Tailscale 순정에서는 --advertise-routes가 정상 동작한다. 이를 활용하면 Cilium이 Pod 패킷을 캡슐화 없이 직접 라우팅할 수 있다.

graph LR
    subgraph VXLAN["VXLAN (기존)"]
        direction LR
        A1[Pod A] --> B1["VXLAN 캡슐화<br/>+50B 오버헤드"]
        B1 --> C1["Tailscale 암호화"]
        C1 --> D1["VXLAN 역캡슐화"]
        D1 --> E1[Pod B]
    end

    subgraph Native["Native Routing (Tailscale 순정)"]
        direction LR
        A2[Pod A] --> C2["Tailscale 암호화<br/>(직접 라우팅)"]
        C2 --> E2[Pod B]
    end

    style VXLAN stroke:#ea580c,stroke-width:2px
    style Native stroke:#16a34a,stroke-width:2px

비교:

항목 VXLAN Native Routing
캡슐화 VXLAN 헤더 추가 (+50B) 없음
MTU 1200 (1280 - VXLAN 오버헤드) 1266 (tailscale0 MTU - Ethernet 헤더 보정)
패킷 경로 Pod → VXLAN → Tailscale → VXLAN → Pod Pod → Tailscale → Pod
Tailscale Route 광고 불필요 필요 (노드별 Pod CIDR /24)
CPU 사용 캡슐화/역캡슐화 처리 직접 라우팅
디버깅 tcpdump에서 내부 패킷 확인 어려움 패킷 경로 추적 용이

4. Cilium 설치 실행

Tailscale 순정 + Native Routing에 최적화된 설정:

helm install cilium cilium/cilium \
  --version 1.18.7 \
  --namespace kube-system \
  --set operator.replicas=1 \
  --set operator.resources.limits.cpu="200m" \
  --set operator.resources.limits.memory="256Mi" \
  --set ipam.mode=kubernetes \
  --set routingMode=native \
  --set ipv4NativeRoutingCIDR=10.244.0.0/16 \
  --set autoDirectNodeRoutes=false \
  --set kubeProxyReplacement=true \
  --set k8sServiceHost=$TAILSCALE_IP \
  --set k8sServicePort=6443 \
  --set bpf.masquerade=true \
  --set loadBalancer.mode=snat \
  --set bpf.lbExternalClusterIP=true \
  --set enableIPv4Masquerade=true \
  --set hostPort.enabled=true \
  --set nodePort.enabled=true \
  --set mtu=1266 \
  --set ipv4.enabled=true \
  --set ipv6.enabled=false \
  --set image.pullPolicy=IfNotPresent \
  --set hubble.relay.enabled=false \
  --set hubble.ui.enabled=false \
  --set prometheus.enabled=false

주요 설정 설명

옵션 이유
routingMode=native 직접 라우팅 VXLAN 캡슐화 제거, Tailscale Route 광고 활용
ipv4NativeRoutingCIDR 10.244.0.0/16 Native Routing 필수. 이 CIDR 내 트래픽은 masquerade 하지 않음
autoDirectNodeRoutes=false Tailscale 위임 Tailscale의 policy routing(table 52)이 노드 간 경로를 관리. Cilium이 직접 경로 설치 시 Tailscale의 next-hop을 해석하지 못해 실패함
kubeProxyReplacement=true eBPF 사용 kube-proxy 대체
mtu=1266 tailscale0 MTU - 14 Cilium BPF의 bpf_fib_lookup()이 L2 길이(IP + Ethernet 14B)를 MTU와 비교하므로, tailscale0 MTU(1280) - Ethernet 헤더(14) = 1266으로 설정해야 FRAG_NEEDED drop 방지
bpf.masquerade=true BPF SNAT Pod → 외부 트래픽의 소스 IP를 노드 IP로 변환
loadBalancer.mode=snat SNAT 리턴 패킷 라우팅 보장

VXLAN 버전과의 차이

- --set routingMode=tunnel \
- --set tunnelProtocol=vxlan \
+ --set routingMode=native \
+ --set ipv4NativeRoutingCIDR=10.244.0.0/16 \
  --set autoDirectNodeRoutes=false \   # 동일 (Tailscale이 라우팅 관리)
- --set mtu=1200 \
+ --set mtu=1266 \

autoDirectNodeRoutes=false인가? Tailscale은 policy routing(table 52)을 사용한다. Cilium의 autoDirectNodeRoutes는 main routing table에 10.244.1.0/24 via 100.x.x.x 형태의 경로를 설치하려 하지만, next-hop(100.x.x.x)이 main table에 없어(table 52에만 존재) RTNETLINK: invalid gateway 에러로 실패한다. 대신 Tailscale의 --advertise-routes + --accept-routes가 table 52에 경로를 추가하고, ip rule(from all lookup 52)이 모든 트래픽을 table 52로 라우팅한다.

5. Cilium CLI 설치 및 확인

# Cilium CLI 설치
CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
CLI_ARCH=arm64
curl -L --fail --remote-name-all \
  https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-${CLI_ARCH}.tar.gz
sudo tar xzvfC cilium-linux-${CLI_ARCH}.tar.gz /usr/bin
rm cilium-linux-${CLI_ARCH}.tar.gz

# 상태 확인
cilium status --wait

# 노드 Ready 확인
kubectl get nodes

6. Cilium 동작 확인

# Cilium Pod 상태
kubectl -n kube-system get pods -l k8s-app=cilium

# Cilium 노드 목록 (Tailscale IP 확인)
kubectl -n kube-system exec ds/cilium -- cilium node list

# 라우팅 모드 확인 (Native인지 확인)
kubectl -n kube-system exec ds/cilium -- cilium status | grep "KubeProxyReplacement"
kubectl -n kube-system exec ds/cilium -- cilium status | grep -i routing
# 예상: Routing Mode: native

# Masquerade 인터페이스 확인
kubectl -n kube-system exec ds/cilium -- cilium status --verbose | grep Masq
# 예상: Masquerading: BPF [enp0s6, tailscale0]

# Native Route 확인 (VXLAN 인터페이스가 없어야 정상)
ip link show | grep vxlan
# 예상: 출력 없음 (VXLAN 미사용)

# Tailscale policy routing 확인 (워커 노드의 Pod CIDR이 table 52에 존재)
ip route show table 52
# 예상: 10.244.1.0/24 dev tailscale0 (워커 추가 후)

# CoreDNS 정상 동작 확인
kubectl get pods -n kube-system -l k8s-app=kube-dns

Phase 6: 워커 노드 추가

노드 구성 예시

이 가이드에서는 4노드 클러스터를 구성한다. 워커 노드는 역할에 따라 라벨을 부여한다.

노드 호스트명 역할 용도
Master instance-20260210-1031 control-plane, infra K8s 컨트롤플레인 + PostgreSQL, Grafana 등 인프라
Worker 1 instance-20260214-1524 gateway Ingress Gateway, 외부 트래픽 처리
Worker 2 instance-20260214-1643 app 애플리케이션 서비스
Worker 3 instance-20260214-1526 app 애플리케이션 서비스

1. Join Token 생성 (마스터에서)

# 새로운 join 명령 생성
kubeadm token create --print-join-command

# 출력 예시:
# kubeadm join 100.88.145.14:6443 --token xxxxx --discovery-token-ca-cert-hash sha256:xxxxx

2. 각 워커에서 Join (워커 3개 모두 반복)

사전 조건: 각 워커에서 Phase 1(containerd), Phase 2(k8s 패키지), Phase 3(--accept-routes) 완료

# Tailscale IP 설정
TAILSCALE_IP=$(tailscale ip -4)
echo "Worker Tailscale IP: $TAILSCALE_IP"

# kubelet node-ip 설정
echo "KUBELET_EXTRA_ARGS=--node-ip=$TAILSCALE_IP" | sudo tee /etc/sysconfig/kubelet

# Join 실행 (마스터에서 받은 명령 사용)
sudo kubeadm join <MASTER_TAILSCALE_IP>:6443 \
  --token <TOKEN> \
  --discovery-token-ca-cert-hash sha256:<HASH>

3. 워커별 Pod CIDR Route 광고

Join 후 Kubernetes가 각 워커에 Pod CIDR을 순차 할당한다. 각 워커가 자신의 CIDR을 Tailscale에 광고해야 다른 노드에서 해당 워커의 Pod에 접근할 수 있다.

마스터에서 전체 노드의 Pod CIDR 확인:

kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'
# 예상 출력:
# instance-20260210-1031  10.244.0.0/24
# instance-20260214-1524  10.244.1.0/24
# instance-20260214-1643  10.244.2.0/24
# instance-20260214-1526  10.244.3.0/24

각 워커에서 자신의 Pod CIDR 광고:

# Worker 1 (instance-20260214-1524)
sudo tailscale set --advertise-routes=10.244.1.0/24

# Worker 2 (instance-20260214-1643)
sudo tailscale set --advertise-routes=10.244.2.0/24

# Worker 3 (instance-20260214-1526)
sudo tailscale set --advertise-routes=10.244.3.0/24

Route 승인: autoApprovers가 정상 설정되었으면 자동 승인된다. 미설정 시 Admin Console에서 노드마다 수동 승인.

4. 노드 역할 라벨링 (마스터에서)

# 노드 상태 확인
kubectl get nodes -o wide

# 워커 역할 표시 (ROLES 컬럼)
kubectl label node instance-20260214-1524 node-role.kubernetes.io/worker=""
kubectl label node instance-20260214-1643 node-role.kubernetes.io/worker=""
kubectl label node instance-20260214-1526 node-role.kubernetes.io/worker=""

# 워크로드 배치용 커스텀 라벨
kubectl label node instance-20260210-1031 node-role=infra
kubectl label node instance-20260214-1524 node-role=gateway
kubectl label node instance-20260214-1643 node-role=app
kubectl label node instance-20260214-1526 node-role=app

5. Route 광고 확인 (마스터에서)

# Tailscale table 52에 모든 워커의 Pod CIDR이 존재하는지 확인
ip route show table 52
# 예상:
# 10.244.1.0/24 dev tailscale0
# 10.244.2.0/24 dev tailscale0
# 10.244.3.0/24 dev tailscale0
# 100.x.x.x dev tailscale0  (각 워커의 Tailscale IP)

# 라벨 확인
kubectl get nodes --show-labels | grep node-role

워커에서도 확인: 각 워커의 ip route show table 52에 마스터의 Pod CIDR(10.244.0.0/24)과 다른 워커들의 Pod CIDR이 보여야 한다.

Phase 7: 클러스터 검증

1. 기본 통신 테스트

# 테스트 Pod 생성
kubectl run test-pod --image=nginx:alpine --restart=Never

# Pod 상태 확인
kubectl get pods -o wide

# Pod 로그 확인
kubectl logs test-pod

# Pod 삭제
kubectl delete pod test-pod

2. 크로스 노드 통신 테스트

# 각 노드에 Pod 배포 (nicolaka/netshoot: 네트워크 디버깅 도구 포함)
# <master-node-name>, <worker-node-name>은 kubectl get nodes에서 확인
kubectl run test-master --image=nicolaka/netshoot --restart=Never \
  --overrides='{"spec":{"nodeName":"<master-node-name>"}}' \
  -- sleep 3600

kubectl run test-worker --image=nicolaka/netshoot --restart=Never \
  --overrides='{"spec":{"nodeName":"<worker-node-name>"}}' \
  -- sleep 3600

# Pod IP 확인
kubectl get pods -o wide

# 크로스 노드 ping 테스트
kubectl exec test-master -- ping -c 3 <worker-pod-ip>
kubectl exec test-worker -- ping -c 3 <master-pod-ip>

# DNS 테스트
kubectl exec test-master -- nslookup kubernetes.default
kubectl exec test-worker -- nslookup kubernetes.default

# Native Routing 확인: traceroute로 패킷 경로 추적
kubectl exec test-master -- traceroute -n <worker-pod-ip>
# 예상: VXLAN 캡슐화 없이 직접 도달 (홉 수 적음)

# 테스트 정리
kubectl delete pod test-master test-worker

3. Service 통신 테스트

# Deployment 생성
kubectl create deployment nginx --image=nginx:alpine --replicas=2

# Service 노출
kubectl expose deployment nginx --port=80 --type=ClusterIP

# Service 확인
kubectl get svc nginx

# 테스트 Pod에서 Service 접근 (curl 사용)
kubectl run test --rm -it --image=nicolaka/netshoot --restart=Never -- curl -s nginx

# 또는 간단한 HTTP 테스트
kubectl run test --rm -it --image=curlimages/curl --restart=Never -- curl -s nginx

# 정리
kubectl delete deployment nginx
kubectl delete svc nginx

4. Cilium Health 확인

# Cilium 연결성 테스트 (시간이 걸림)
cilium connectivity test

# Health 상태
kubectl -n kube-system exec ds/cilium -- cilium-health status

Phase 8: 마스터 노드 워크로드 스케줄링 설정 (옵셔널)

이 섹션은 VXLAN 버전과 동일하다. 자세한 내용은 03-setup-kubernetes-cilium.md - Phase 7을 참고.

요약:

구성 장점 단점 권장 환경
Taint 제거 리소스 효율 컨트롤플레인 위험 개발/테스트, 노드 1-2개
Taint 유지 안정성 최우선 리소스 제약 프로덕션, 노드 3개 이상

체크리스트

필수 작업

  • Containerd 설치 및 실행
  • Kubernetes 패키지 설치
  • Tailscale 설치 및 연결 (--accept-routes)
  • Tailscale ACL autoApprovers 설정
  • 마스터 노드 초기화 성공
  • 마스터 Pod CIDR Route 광고 (10.244.0.0/24)
  • kubectl 설정 완료
  • Cilium CNI 설치 (Native Routing 모드)
  • cilium status에서 Routing Mode: native 확인
  • 노드 Ready 상태 확인
  • CoreDNS 정상 동작
  • 워커 노드 Join
  • 워커 Pod CIDR Route 광고 (10.244.x.0/24)
  • ip route show table 52에서 상대 노드 Pod CIDR 확인
  • Pod 간 크로스 노드 통신 성공
  • Service 통신 성공

옵셔널

  • 마스터 노드 taint 설정 결정 (유지 또는 제거)
  • 워커 노드 역할 라벨링
  • cilium connectivity test 통과

트러블슈팅

노드가 NotReady 상태

# kubelet 로그 확인
sudo journalctl -u kubelet -f

# CNI 플러그인 확인
ls -la /opt/cni/bin/

Pod 간 크로스 노드 통신 불가

Native Routing에서 가장 흔한 문제. 단계별로 진단:

# 1. 각 노드가 자신의 Pod CIDR(/24)을 광고 중인지 확인
tailscale debug prefs | grep AdvertiseRoutes
# 마스터: [10.244.0.0/24 10.96.0.0/12]
# 워커:   [10.244.1.0/24]
# ⚠️ 모든 노드가 10.244.0.0/16을 광고하면 안 됨! (HA 충돌)

# 2. Tailscale table 52에 상대 노드의 Pod CIDR이 있는지 확인
ip route show table 52
# 마스터에서: 10.244.1.0/24 dev tailscale0 이 보여야 함
# 워커에서:   10.244.0.0/24 dev tailscale0 이 보여야 함

# 3. 위 경로가 없으면: Route 승인 확인
# Tailscale Admin Console → Machines → 해당 노드 → Subnet routes
# "Awaiting Approval"이면 수동 승인 필요

# 4. ip rule에 table 52 참조가 있는지 확인
ip rule show | grep "lookup 52"
# 예상: 5270: from all lookup 52

# 5. Cilium 노드 목록 확인
kubectl -n kube-system exec ds/cilium -- cilium node list

# 6. IP 포워딩 활성화 확인
sysctl net.ipv4.ip_forward
# 예상: net.ipv4.ip_forward = 1

핵심: 크로스 노드 통신은 Tailscale의 --advertise-routes(노드별 /24) + --accept-routes + Route 승인, 이 세 가지가 모두 갖춰져야 동작한다.

크로스 노드 TCP 전송이 간헐적으로 stall (일부 바이트만 수신)

증상: 크로스 노드 HTTP 요청에서 응답이 부분적으로만 수신되고 나머지는 타임아웃. 작은 응답(< 1KB)은 성공하지만 큰 응답(> 10KB)은 간헐적으로 실패. 브라우저에서 ERR_CONTENT_LENGTH_MISMATCH 또는 ERR_INCOMPLETE_CHUNKED_ENCODING 에러.

원인: Cilium BPF의 bpf_fib_lookup()이 패킷의 L2 길이(IP + Ethernet 14B)를 출력 인터페이스의 L3 MTU와 비교하여, MTU 크기의 패킷이 BPF_FIB_LKUP_RET_FRAG_NEEDED(코드 8)로 drop됨.

예: tailscale0 MTU = 1280
    IP 패킷 = 1280B, Ethernet 포함 = 1294B
    BPF: 1294 > 1280 → FRAG_NEEDED → drop!

진단:

# Cilium drop 모니터링 (Grafana 등 대상 pod이 있는 노드의 Cilium에서)
kubectl exec -n kube-system <cilium-pod> -- cilium-dbg monitor --type drop
# "FIB lookup failed, 8" 메시지가 보이면 이 문제

# Drop 메트릭 확인
kubectl exec -n kube-system <cilium-pod> -- cilium-dbg metrics list | grep "FIB"
# cilium_drop_count_total ... reason=FIB lookup failed 카운트 증가 확인

해결: Cilium MTU를 tailscale0 MTU - Ethernet 헤더(14B)로 설정:

# MTU 1266 = 1280 (tailscale0) - 14 (Ethernet header)
helm upgrade cilium cilium/cilium --reuse-values --set mtu=1266
kubectl rollout restart daemonset/cilium -n kube-system

왜 1280이 아니라 1266인가? Cilium의 BPF 코드가 bpf_fib_lookup() 호출 시 params.tot_len에 L2 프레임 길이(ctx->len, Ethernet 헤더 포함)를 전달하지만, 커널은 이를 출력 인터페이스의 L3 MTU(dst_mtu())와 비교한다. MTU 1280인 경우 최대 IP 패킷도 1280B이므로 L2 길이는 1294B가 되어 MTU를 초과한다. lxc veth MTU를 1266으로 설정하면 최대 IP 패킷 = 1266B, L2 = 1280B = tailscale0 MTU로 정확히 맞는다.

Cilium Pod CrashLoopBackOff

새 워커 노드 추가 시 문제

다음 단계

클러스터가 정상 동작하면:
04-deep-dive-networking.md - 네트워킹 심화 이해

추가 팁

리소스 최적화

Control Plane (2 OCPU, 12GB):

# kubelet 리소스 예약
sudo tee -a /var/lib/kubelet/config.yaml <<EOF
systemReserved:
  cpu: "200m"
  memory: "512Mi"
kubeReserved:
  cpu: "200m"
  memory: "512Mi"
EOF

Worker Node (4 OCPU, 24GB):

# 더 많은 Pod 허용
sudo tee -a /var/lib/kubelet/config.yaml <<EOF
maxPods: 110
systemReserved:
  cpu: "300m"
  memory: "1Gi"
EOF

모니터링 추가

# Metrics Server 설치
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

# ARM64 호환성 패치
kubectl patch deployment metrics-server -n kube-system --type='json' \
  -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--kubelet-insecure-tls"}]'

# 확인
kubectl top nodes
kubectl top pods

참고 자료

관련 블로그

공식 문서

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함