Nối Mạng Pod Bằng Tay: CNI bridge và Route VPC

K
Kai··8 min read

Bài 13 dựng nền lý thuyết: mô hình mạng phẳng, IP-per-pod, và lựa chọn native routing cho cụm một-subnet của ta. Giờ lắp ráp. Việc gói gọn trong ba mảnh: viết CNI config để mỗi node tự cấp IP và bridge cho pod của mình, thêm route trong VPC để gói pod-to-pod tìm được đường xuyên node, và một luật masquerade cho lưu lượng pod ra ngoài cluster. Xong ba mảnh đó, node tự chuyển sang Ready và pod chạy được thật.

Bước 1 — CNI config cho mỗi worker

Nhớ lại Bài 10–13: binary plugin đã nằm sẵn trong /opt/cni/bin, nhưng /etc/cni/net.d còn rỗng, và cái rỗng đó là lý do node NotReady. Giờ viết hai file config vào đó.

File chính dùng plugin bridge với IPAM host-local. Điểm cốt lõi: mỗi node khai một subnet khác nhau (worker-0 lấy 10.200.0.0/24, worker-1 lấy 10.200.1.0/24) để hai node không bao giờ cấp trùng IP pod.

# trên worker-0 (worker-1 đổi subnet thành 10.200.1.0/24)
sudo mkdir -p /etc/cni/net.d
sudo tee /etc/cni/net.d/10-bridge.conf >/dev/null <<'EOF'
{
  "cniVersion": "1.0.0",
  "name": "bridge",
  "type": "bridge",
  "bridge": "cni0",
  "isGateway": true,
  "ipMasq": false,
  "ipam": {
    "type": "host-local",
    "ranges": [[{"subnet": "10.200.0.0/24"}]],
    "routes": [{"dst": "0.0.0.0/0"}]
  }
}
EOF

sudo tee /etc/cni/net.d/99-loopback.conf >/dev/null <<'EOF'
{
  "cniVersion": "1.0.0",
  "name": "lo",
  "type": "loopback"
}
EOF

Từng trường:

  • type: bridge, bridge: cni0 — mọi pod trên node treo vào một cầu ảo tên cni0. Pod cùng node nói chuyện qua cầu này ở tầng 2.
  • isGateway: true — gán cho cni0 địa chỉ .1 của dải (10.200.0.1) và biến nó thành cổng mặc định cho pod. Gói pod đi đâu cũng qua đây trước.
  • ipMasq: falsekhông để plugin tự masquerade. Đây là lựa chọn quan trọng của native routing: ta muốn gói pod-to-pod giữ nguyên IP nguồn thật (mô hình "không NAT" ở Bài 13). Việc masquerade cho lưu lượng ra ngoài cluster ta làm riêng, có kiểm soát, ở Bước 3.
  • ipam: host-local với ranges là dải của node — plugin tự quản lý việc cấp IP tuần tự trong dải, lưu sổ ở /var/lib/cni/networks. routes: 0.0.0.0/0 ghi vào pod một route mặc định trỏ về cni0.
  • File 99-loopback dựng interface lo bên trong mỗi pod. Tên có số 99 để nó nạp sau; plugin loopback chỉ lo 127.0.0.1 nội bộ pod.

containerd theo dõi thư mục này và nạp lại ngay. Kiểm tra nó đã đọc được config chưa:

sudo crictl info | grep -i lastCNILoadStatus
  "lastCNILoadStatus": "OK",

OK thay cho thông báo cni plugin not initialized ở các bài trước, runtime giờ biết cách dựng mạng pod. Viết y hệt cho worker-1 với subnet 10.200.1.0/24.

Bước 2 — Route trong VPC cho hai dải pod

Config trên đủ để pod cùng node nói chuyện với nhau qua cni0. Nhưng gói từ pod ở worker-0 (10.200.0.x) gửi tới pod ở worker-1 (10.200.1.x) sẽ đi tới cni0, ra eth0 của node, rồi tới router của VPC — và router không biết 10.200.1.0/24 nằm ở đâu, vì đó là dải pod, không phải dải của subnet. Ta dạy nó bằng hai route tĩnh: dải pod nào thì gửi tới instance giữ dải đó.

RTB=rtb-086c1b93e4ff0a50c   # route table của subnet
aws ec2 create-route --route-table-id $RTB \
  --destination-cidr-block 10.200.0.0/24 --instance-id i-0f1ab7628507cb9cd  # worker-0
aws ec2 create-route --route-table-id $RTB \
  --destination-cidr-block 10.200.1.0/24 --instance-id i-0a33782c408f5bf09  # worker-1
{ "Return": true }
{ "Return": true }

Xem lại bảng định tuyến:

aws ec2 describe-route-tables --route-table-ids $RTB \
  --query 'RouteTables[].Routes[].[DestinationCidrBlock,InstanceId,GatewayId]' --output text
10.200.0.0/24   i-0f1ab7628507cb9cd   None
10.200.1.0/24   i-0a33782c408f5bf09   None
10.0.0.0/16     None                  local
0.0.0.0/0       None                  igw-0f956a0362900fb68

Hai dòng đầu là phần ta vừa thêm, và đây là "native routing" của Bài 13 ở dạng cụ thể: mạng dưới (VPC) tự định tuyến dải pod, không cần đường hầm. Route này chạy được là nhờ ở Bài 3 ta đã tắt source/destination check trên hai instance; nếu không, AWS sẽ chặn node chuyển tiếp những gói mang IP pod (không phải IP của chính node).

Bước 3 — Masquerade cho lưu lượng pod ra ngoài cluster

Vì để ipMasq: false, pod-to-pod giữ nguyên IP. Nhưng khi pod muốn ra Internet (hay tới một dịch vụ ngoài cluster) thì gói mang IP nguồn 10.200.x.y, và đích đó không có đường trả về dải pod. Cần SNAT những gói rời khỏi mạng pod về IP node, và chỉ những gói đó, tuyệt đối không đụng pod-to-pod.

Điều kiện diễn đạt gọn: nguồn thuộc dải pod của node, đích không thuộc dải pod tổng (10.200.0.0/16):

# trên worker-0 (worker-1 đổi -s thành 10.200.1.0/24)
sudo iptables -t nat -A POSTROUTING \
  -s 10.200.0.0/24 ! -d 10.200.0.0/16 -j MASQUERADE \
  -m comment --comment "pod egress masq"

! -d 10.200.0.0/16 là mấu chốt: mọi đích trong mạng pod được loại khỏi masquerade, nên pod-to-pod (kể cả xuyên node) vẫn thấy IP thật của nhau; chỉ lưu lượng đi ra ngoài 10.200.0.0/16 mới bị SNAT về IP node.

Bước 4 — Node chuyển sang Ready

Không cần restart gì. kubelet vẫn đang hỏi containerd về tình trạng mạng theo chu kỳ; ngay khi CNI nạp OK, điều kiện Ready của node tự lật:

kubectl get nodes -o wide
NAME       STATUS   ROLES    AGE   VERSION   INTERNAL-IP   CONTAINER-RUNTIME
worker-0   Ready    <none>   14m   v1.36.1   10.0.1.20     containerd://2.3.1
worker-1   Ready    <none>   13m   v1.36.1   10.0.1.21     containerd://2.3.1

Ready, cả hai. Đây là cột mốc: lần đầu cluster sẵn sàng nhận và chạy pod thật. Lỗ hổng kéo dài từ Bài 11 đã được lấp đúng chỗ ta dự đoán: là CNI, không phải kubelet hỏng.

Bước 5 — Tạo pod thật trên hai node

Tạo hai pod busybox, ghim mỗi cái vào một node bằng nodeName để chắc chắn chúng nằm khác node — đúng tình huống ta muốn thử:

cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: pod-a
spec:
  nodeName: worker-0
  containers:
  - name: app
    image: busybox:1.36
    command: ["sleep", "3600"]
---
apiVersion: v1
kind: Pod
metadata:
  name: pod-b
spec:
  nodeName: worker-1
  containers:
  - name: app
    image: busybox:1.36
    command: ["sleep", "3600"]
EOF

kubectl wait --for=condition=Ready pod/pod-a pod/pod-b --timeout=90s
kubectl get pods -o wide
NAME    READY   STATUS    RESTARTS   AGE   IP           NODE
pod-a   1/1     Running   0          7s    10.200.0.2   worker-0
pod-b   1/1     Running   0          7s    10.200.1.2   worker-1

Hai pod Running, và quan trọng là IP: pod-a nhận 10.200.0.2 (từ dải của worker-0), pod-b nhận 10.200.1.2 (từ dải của worker-1). IPAM host-local đã cấp đúng dải mỗi node: .1cni0, nên pod đầu tiên nhận .2. Nhìn từ trong pod, route khớp với những gì ta cấu hình:

kubectl exec pod-a -- ip route
default via 10.200.0.1 dev eth0
10.200.0.0/24 dev eth0 scope link  src 10.200.0.2

Cổng mặc định là 10.200.0.1, chính cni0 của worker-0. Mọi thứ pod gửi ra ngoài dải node mình đều qua đây.

Bước 6 — Ping xuyên node

Đây là phép thử của cả ba bài mạng. pod-a trên worker-0 ping pod-b trên worker-1:

kubectl exec pod-a -- ping -c 3 10.200.1.2
PING 10.200.1.2 (10.200.1.2): 56 data bytes
64 bytes from 10.200.1.2: seq=0 ttl=62 time=1.369 ms
64 bytes from 10.200.1.2: seq=1 ttl=62 time=0.347 ms
64 bytes from 10.200.1.2: seq=2 ttl=62 time=0.410 ms

--- 10.200.1.2 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss

Thông được, không mất gói. ttl=62 (giảm 2 so với 64 mặc định) cho thấy gói qua đúng hai chặng định tuyến, tức cni0 của worker-0 rồi VPC chuyển tới worker-1, chứ không phải đường hầm. Đường đi đầy đủ:

   pod-a 10.200.0.2                          pod-b 10.200.1.2
       │ veth                                     veth │
   ┌───┴── cni0 10.200.0.1                cni0 10.200.1.1 ──┴───┐
   │   worker-0 eth0 10.0.1.20      worker-1 eth0 10.0.1.21     │
   └────────┬──────────────────────────────────┬───────────────┘
            └──────► VPC route table ◄──────────┘
                 10.200.1.0/24 → worker-1
                 10.200.0.0/24 → worker-0

Còn ba điều kiểm chứng cho trọn. Pod nhìn thấy đúng IP thật của mình (không nấp sau NAT):

kubectl exec pod-a -- ip -4 addr show eth0 | grep inet
    inet 10.200.0.2/24 brd 10.200.0.255 scope global eth0

Từ trong pod, mở TCP tới ClusterIP của Service kubernetes; kube-proxy (Bài 12) làm việc của nó ngay cả khi nguồn là một pod thật:

kubectl exec pod-b -- nc -w 3 -zv 10.32.0.1 443
10.32.0.1 (10.32.0.1:443) open

Và pod ra được Internet, nhờ luật masquerade ở Bước 3:

kubectl exec pod-b -- ping -c 2 8.8.8.8
2 packets transmitted, 2 packets received, 0% packet loss

Bốn kiểu giao tiếp của Bài 13 giờ đều chạy thật: container trong pod (localhost), pod-to-pod cùng node (cni0), pod-to-pod xuyên node (route VPC), và pod-to-Service (kube-proxy).

🧹 Dọn dẹp

Xóa hai pod thử, chúng chỉ để kiểm chứng:

kubectl delete pod pod-a pod-b

CNI config và route VPC là phần thường trú của cluster, giữ nguyên. Một lưu ý về độ bền: route VPC tồn tại qua stop/start, nhưng luật iptables masquerade ở Bước 3 thì không, vì nó nằm trong bộ nhớ kernel và mất sau khi reboot/stop-start instance. Nếu bạn tạm dừng cụm rồi bật lại, chạy lại lệnh iptables ... MASQUERADE trên mỗi worker (hoặc đặt nó vào một systemd unit oneshot nếu muốn bền hẳn). Các rule của kube-proxy và bridge thì tự dựng lại khi service khởi động, không cần lo.

Script đầy đủ (CNI config hai node, lệnh route, masquerade) ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 14-pod-network.

Tổng kết

Mạng pod đã thông từ đầu đến cuối, dựng hoàn toàn bằng tay và bằng những mảnh ta hiểu rõ: cni0 cho pod cùng node, route VPC cho xuyên node, masquerade có chọn lọc cho lưu lượng ra ngoài. Không overlay, không header thừa: gói pod-to-pod đi nguyên vẹn vì cụm gọn trong một subnet và ta kiểm soát được định tuyến của mạng dưới. Hai node Ready, pod chạy thật và liên lạc được mọi hướng.

Một thứ vẫn còn thiếu để cluster ra dáng dùng được: pod gọi nhau bằng tên, không phải IP. IP pod thay đổi mỗi lần pod tái sinh, nên không ai hard-code chúng. Bài 15 triển khai CoreDNS như một Deployment trong cluster, tạo Service cho nó ở đúng 10.32.0.10 mà kubelet đã trỏ pod tới (Bài 11), và xem một pod phân giải được tên Service thành ClusterIP.

Related Posts