Repository Reading Site
集群初始化 — 从裸机到 5 节点集群
我们的服务器都是云 VPS,公网 IP 通过 NAT 映射,本机网卡上只有内网 IP: Kubernetes 的 kubelet 需要通过 `--node-ip` 告诉 Master "我的 IP 是什么",API Server 用这个 IP 来连接 kubelet(比如 `kubectl logs`、`kubectl exec`)。 WireGuard 是
集群初始化 — 从裸机到 5 节点集群
跨机房网络方案:WireGuard 虚拟内网
问题:公网 IP 不在本地网卡上
我们的服务器都是云 VPS,公网 IP 通过 NAT 映射,本机网卡上只有内网 IP:
# 107.148.176.193 (Master) 上实际看到的:
$ ip addr show
inet 10.2.207.3/24 # 只有这个内网 IP
# 公网 IP 107.148.176.193 是 NAT 网关映射的,不在本机
# 38.76.221.17 (HK Worker) 上也是:
inet 10.4.225.2/24 # 内网 IP
# 但有些机器公网 IP 直接绑定在网卡上:
# 154.9.27.60: inet 154.9.27.60/24 ← 公网IP在网卡上
# 154.219.104.66: inet 154.219.104.66/24
为什么这是个问题?
Kubernetes 的 kubelet 需要通过 --node-ip 告诉 Master "我的 IP 是什么",API Server 用这个 IP 来连接 kubelet(比如 kubectl logs、kubectl exec)。
- 如果用内网 IP(10.x.x.x):不同机房的内网不互通
- 如果用公网 IP:kubelet 要求这个 IP 必须在本地网卡上,NAT 场景下会报错
Failed to set some node status fields:
node IP: "38.76.221.17" not found in the host's network interfaces
解决方案:WireGuard 组虚拟内网
WireGuard 是 Linux 内核级 VPN,在每台机器上创建一个虚拟网卡 wg0,分配一个统一的私有 IP:
机器公网 IP WireGuard IP (wg0 网卡)
107.148.176.193 → 10.10.0.1 (Master)
107.148.164.118 → 10.10.0.2 (Worker-1)
154.9.27.60 → 10.10.0.3 (Worker-2)
38.76.221.17 → 10.10.0.4 (Worker-3)
154.219.104.66 → 10.10.0.5 (Worker-4)
WireGuard IP 在本地网卡上(wg0 接口),kubelet 接受它。流量通过加密隧道走公网传输。
WireGuard 安装和配置
# 安装(所有节点)
apt-get install -y wireguard-tools
# 生成密钥(每台机器上执行)
wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey
chmod 600 /etc/wireguard/privatekey
每台机器的配置文件 /etc/wireguard/wg0.conf(以 Master 为例):
[Interface]
Address = 10.10.0.1/24 # 本机的 WireGuard IP
PrivateKey = <本机私钥>
ListenPort = 51820 # WireGuard 监听端口
[Peer]
PublicKey = <Worker-1 的公钥>
AllowedIPs = 10.10.0.2/32 # 只路由这个对端的 WireGuard IP
Endpoint = 107.148.164.118:51820 # 对端的公网 IP + 端口
PersistentKeepalive = 25 # 每 25 秒发心跳,保持 NAT 映射
# ... 其他 Peer 同理
关键参数解释:
| 参数 | 说明 |
|---|---|
Address |
本机虚拟 IP,写在 wg0 网卡上 |
PrivateKey |
本机私钥,不能泄露 |
ListenPort |
UDP 监听端口,所有 peer 通过这个端口通信 |
PublicKey |
对端公钥,用于加密验证 |
AllowedIPs |
允许从这个 peer 收到的源 IP 范围,也决定路由 |
Endpoint |
对端的公网地址 |
PersistentKeepalive |
NAT 穿透必须,否则 NAT 映射超时后连接断开 |
# 启动并设置开机自启
systemctl enable --now wg-quick@wg0
验证结果
From Master (10.10.0.1):
→ 10.10.0.2 (Worker-1, LA): 1.45ms ← 同城
→ 10.10.0.3 (Worker-2, LA): 1.69ms ← 同城
→ 10.10.0.4 (Worker-3, HK): 155ms ← 跨洋
→ 10.10.0.5 (Worker-4, HK): 158ms ← 跨洋
面试考点: 为什么选 WireGuard?
- 内核级实现,性能接近裸机(比 OpenVPN 快很多)
- 代码量极小(约 4000 行),攻击面小
- 使用现代密码学(Curve25519, ChaCha20, Poly1305)
- 配置简单,无需 CA 证书体系
kubeadm init — 初始化控制平面
配置文件
直接用命令行参数容易出错,推荐用配置文件:
# /root/kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: "10.10.0.1" # API Server 绑定的 IP(WireGuard IP)
bindPort: 6443
nodeRegistration:
kubeletExtraArgs:
node-ip: "10.10.0.1" # kubelet 上报的节点 IP
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: "v1.30.14"
controlPlaneEndpoint: "10.10.0.1:6443" # ★ 关键:Worker join 时连接的地址
networking:
podSubnet: "10.244.0.0/16" # Pod 网段,必须与 CNI 配置匹配
serviceSubnet: "10.96.0.0/12" # Service ClusterIP 网段
apiServer:
certSANs: # API Server TLS 证书的 SAN
- "107.148.176.193" # 公网 IP(外部访问用)
- "10.10.0.1" # WireGuard IP
- "10.2.207.3" # 原始内网 IP
- "127.0.0.1" # localhost
每个参数的意义
advertiseAddress vs controlPlaneEndpoint
这是最容易搞混的两个参数:
| 参数 | 作用 | 存在哪 |
|---|---|---|
advertiseAddress |
API Server 绑定监听的 IP | 只在 Master 本机 |
controlPlaneEndpoint |
Worker join 时连接的地址,写入 cluster-info ConfigMap | 全集群共享 |
我们踩过的坑: 第一次 init 时没设 controlPlaneEndpoint,导致 cluster-info 里存了内网 IP 10.2.207.3。Worker join 时先通过我们指定的公网 IP 连上了 API Server(bootstrap 阶段),但随后从 cluster-info 读取到内网 IP 并切换过去——HK 的 Worker 根本连不到 LA 的内网 IP,join 超时。
教训: controlPlaneEndpoint 必须设为所有节点都能到达的地址。
podSubnet: "10.244.0.0/16"
K8s 中每个 Pod 都有自己的 IP 地址,这些 IP 从 Pod 网段中分配。
- 这个网段不能和节点 IP、Service IP、WireGuard IP 冲突
- 必须和 CNI 插件的配置一致(Calico 默认用
192.168.0.0/16,我们指定10.244.0.0/16需要让 Calico 也知道) /16表示有 65536 个 IP 可用,足够大规模集群
serviceSubnet: "10.96.0.0/12"
Service ClusterIP 的范围。/12 表示有 ~100 万个 ClusterIP 可用。
第一个 ClusterIP 10.96.0.1 会自动分配给 kubernetes 这个默认 Service(即 API Server)。
certSANs — 证书的 Subject Alternative Names
API Server 使用 HTTPS,需要 TLS 证书。证书中的 SAN 字段列出了"这个证书对哪些地址有效"。
如果你用 https://107.148.176.193:6443 访问 API Server,但证书的 SAN 里没有这个 IP,kubectl 会报证书错误。所以我们把所有可能的访问地址都加进去。
执行初始化
kubeadm init --config /root/kubeadm-config.yaml
初始化过程做了什么(按顺序):
- Preflight checks — 检查端口、swap、容器运行时等
- Pull images — 拉取 K8s 组件镜像(API Server、etcd、Scheduler 等)
- Generate certificates — 为 API Server、etcd、kubelet 等生成 TLS 证书和密钥
- Generate kubeconfig — 为各组件生成访问 API Server 的配置文件
- Write static Pod manifests — 在
/etc/kubernetes/manifests/写入 YAML 文件 - Start kubelet — kubelet 检测到 manifests 目录中的文件,启动 static Pod
- Wait for API Server healthy — 等待 API Server 就绪
- Upload config — 将配置存储到 ConfigMap(kubeadm-config、kubelet-config)
- Mark control-plane — 给 Master 节点加标签和 taint
- Generate bootstrap token — 生成 Worker 加入集群的 token
- Install addons — 安装 CoreDNS 和 kube-proxy
面试考点 — Static Pod:
K8s 的控制平面组件(API Server、etcd、Scheduler、Controller Manager)是作为"Static Pod"运行的。kubelet 直接监控 /etc/kubernetes/manifests/ 目录,发现 YAML 就启动对应的容器。这些 Pod 不受 API Server 管理(因为 API Server 还没启动呢),是 kubelet 独立管理的。
安装 CNI — Calico
为什么需要 CNI?
kubeadm init 之后,Master 节点状态是 NotReady,CoreDNS Pod 是 Pending:
NAME STATUS ROLES AGE
us480851516617a NotReady control-plane 22s
原因:K8s 本身不提供 Pod 网络。它定义了网络模型(每个 Pod 一个 IP,Pod 之间可直接通信),但具体实现交给 CNI(Container Network Interface)插件。
没有 CNI = Pod 没有 IP = 无法调度 = 节点 NotReady。
为什么选 Calico?
| CNI 插件 | 优势 | 劣势 |
|---|---|---|
| Calico | 支持 NetworkPolicy、BGP、IPIP、VXLAN | 资源占用略高 |
| Flannel | 简单轻量 | 不支持 NetworkPolicy |
| Cilium | eBPF 加速、强大的安全策略 | 复杂,需要较新内核 |
我们选 Calico 因为它支持 NetworkPolicy(后续要练习网络隔离),同时足够稳定。
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml
安装后状态变化
安装前:Master NotReady,CoreDNS Pending
安装后:Master Ready,CoreDNS Running,Calico DaemonSet 在每个节点部署 calico-node
Worker Join — 加入集群
Join 配置文件
# /root/kubeadm-join-config.yaml
apiVersion: kubeadm.k8s.io/v1beta3
kind: JoinConfiguration
discovery:
bootstrapToken:
apiServerEndpoint: "10.10.0.1:6443" # Master 的 WireGuard IP
token: "<bootstrap-token>"
caCertHashes:
- "sha256:<hash>" # CA 证书指纹,防止中间人攻击
nodeRegistration:
kubeletExtraArgs:
node-ip: "10.10.0.X" # 本节点的 WireGuard IP
Join 过程详解
- Worker 连接
10.10.0.1:6443,用 bootstrap token 做初始认证 - 获取 CA 证书,验证 hash 匹配(防止连到假冒的 API Server)
- 发送 CSR(Certificate Signing Request)请求自己的 kubelet 证书
- API Server 自动签发证书
- kubelet 获得证书后,正式注册为集群节点
- kubelet 开始汇报节点状态,接收 Pod 调度
面试考点 — TLS Bootstrap: Worker 加入集群时的"鸡生蛋"问题:kubelet 需要证书才能和 API Server 通信,但证书是 API Server 签发的。Bootstrap Token 是解决方案——一个临时的低权限凭证,只够用来发送 CSR。
踩过的坑:Cilium eBPF 残留
154.9.27.60 和 154.219.104.66 之前运行过 Cilium CNI。Cilium 使用 eBPF 在内核层面劫持 connect() 系统调用,实现 Service IP 到 Pod IP 的转换。
问题: kubeadm reset 只清理 K8s 文件和 iptables,但不清理 eBPF 程序。Cilium 的 cil_sock4_connect BPF 程序仍然挂载在 cgroup 上,拦截所有 TCP 连接,把 ClusterIP 10.96.0.1 错误地 DNAT 到旧的后端 IP。
排查过程:
curl https://10.10.0.1:6443/healthz→ OK(直连)curl https://10.96.0.1:443/healthz→ 超时(ClusterIP 不通)tcpdump发现 SYN 包被发往了错误的 IP10.0.63.3iptables规则正确(DNAT 到 10.10.0.1:6443),且 0 pkts 命中bpftool prog list发现 Cilium eBPF 程序仍在运行- Detach BPF 程序后 ClusterIP 恢复正常
解决方法:
# 查看挂载的 BPF 程序
bpftool cgroup show /sys/fs/cgroup
# 清除 Cilium BPF 文件
rm -rf /sys/fs/bpf/tc /sys/fs/bpf/cilium
# 清空 nftables 规则(可能有 Cilium 链)
nft flush ruleset
# 清空 conntrack
conntrack -F
教训: 在复用之前运行过其他 K8s/CNI 的机器时,kubeadm reset 是不够的。必须额外清理:
- eBPF 程序(
bpftool) - nftables 规则(
nft flush ruleset) - CNI 配置(
rm -rf /etc/cni/net.d) - conntrack 表(
conntrack -F) - 残留的 Calico/Cilium 数据(
/var/lib/calico、/var/lib/cilium)
最终集群状态
NAME STATUS ROLES VERSION INTERNAL-IP
cp-3 Ready <none> v1.30.14 10.10.0.3 (Worker-2, LA)
hk652699382121 Ready <none> v1.30.14 10.10.0.4 (Worker-3, HK)
us480851516617a Ready control-plane v1.30.14 10.10.0.1 (Master, LA)
us590068728056 Ready <none> v1.30.14 10.10.0.2 (Worker-1, LA)
wk-1 NotReady <none> v1.30.14 10.10.0.5 (Worker-4, HK, 待恢复)
4/5 节点 Ready(Worker-4 机器暂时失联,恢复后清理 Cilium BPF 残留即可)。
VPN 服务(107.148.176.193 和 107.148.164.118 上的 Xray + WARP)全程正常运行,未受影响。
接下来
集群搭建完成后,需要给节点打标签和 taint,为后续的调度练习做准备。
→ 05-cni-networking.md — CNI 网络原理深入 → 06-node-setup.md — 节点标签、Taint 和 kubeconfig 配置