AI Infra 训练营
总览
  • 总览
  • 完整安装
  • 核心 K8s
  • Cilium 网络
  • Longhorn 存储
  • 监控日志
  • CI / GitOps
  • 安全准入
  • CI/CD 实战(MySQL+Go+Vue)
  • HPA/Ingress/Hubble 实战
  • 面试速查 + 真实踩坑
  • Day 0 · 新手接管 Runbook
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
命令手册
HiHuo 主站
GitHub
总览
  • 总览
  • 完整安装
  • 核心 K8s
  • Cilium 网络
  • Longhorn 存储
  • 监控日志
  • CI / GitOps
  • 安全准入
  • CI/CD 实战(MySQL+Go+Vue)
  • HPA/Ingress/Hubble 实战
  • 面试速查 + 真实踩坑
  • Day 0 · 新手接管 Runbook
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
命令手册
HiHuo 主站
GitHub
  • 实操 Runbook

    • Runbook 总览:从零部署、查看、调试
    • 完整安装总 Runbook:5 台 Ubuntu 到可用平台
    • 核心 K8s Runbook:apiserver / etcd / kubelet / containerd / HAProxy
    • Cilium 网络 Runbook:安装、查看、调试
    • Longhorn 存储 Runbook:安装、查看、调试
    • 监控日志 Runbook:Prometheus / Grafana / Loki / Alertmanager
    • CI / GitOps Runbook:Harbor / Gitea / Jenkins / Kaniko / ArgoCD
    • 安全准入 Runbook:RBAC / PSA / Kyverno / ResourceQuota
    • 实战 Runbook:MySQL + Go + Vue 全链路 CI/CD 真实发布
    • 实战 Runbook:给应用加 HPA 自动扩缩 + Ingress 域名 + Hubble 流量观测
    • 面试速查:这套平台 + 高频问答 + 真实踩坑

完整安装总 Runbook:5 台 Ubuntu 到可用平台

这篇回答“我拿到 5 台空 Ubuntu 机器,怎么一步步装到当前这套平台”。如果机器上已经有集群,先不要重跑安装命令,先用 Day 0 新手接管 Runbook 看状态。


0. 安装目标

装完后具备:

  • 3 control plane + 2 worker 的 K8s v1.30 集群。
  • 本地 HAProxy 做 apiserver 高可用入口:k8s-api:16443。
  • containerd 作为容器运行时。
  • Cilium 做 CNI 和 Hubble 流量观测。
  • Longhorn 做默认 StorageClass。
  • kube-prometheus-stack + Loki 做监控和日志。
  • Harbor + Gitea + Jenkins + ArgoCD 做镜像、源码、CI、CD。

组件关系:

kubectl -> k8s-api:16443 -> HAProxy -> 3 个 apiserver
                                   -> etcd 存集群状态

Pod -> Cilium -> 跨节点网络 / Service / NetworkPolicy
PVC -> Longhorn CSI -> 多副本块存储
Prometheus -> ServiceMonitor -> 采集指标 -> Grafana 展示
Promtail -> Loki -> Grafana 查日志
Gitea -> Jenkins/Kaniko -> Harbor -> ArgoCD -> K8s

1. 机器规划

节点公网 IP内网 IPhostname角色
1154.201.73.3110.0.24.31k8s-cp-1init control plane
2154.201.73.8110.0.24.29k8s-cp-2control plane
345.205.31.21410.0.24.32k8s-cp-3control plane
445.205.31.18010.0.24.28k8s-w-1worker
545.205.31.1010.0.24.30k8s-w-2worker

为什么 3 个 control plane:etcd 使用 Raft,多数派为 2,3 节点能容忍 1 台坏掉。2 个 control plane 坏 1 台就没有多数派,不是 HA。

为什么 worker 只有 2 台:学习集群资源有限,业务负载和系统组件可以先混跑;生产应把 control plane 和业务节点隔离。


2. 从本机重建 SSH 通道 + 检查系统

这一节在你自己的电脑(Mac / Linux)上执行,不是在服务器上。

2.0 重装系统后必做:重建 SSH(否则后面每条命令都连不上)

云控制台「重装系统 / 重置」会把机器恢复成全新 OS,带来两个后果,新手最容易卡在这里:

  1. 新系统 = 新 SSH 指纹。你本地 ~/.ssh/known_hosts 里记的还是旧机器指纹,再连会被当成「中间人攻击」直接拒连,报 REMOTE HOST IDENTIFICATION HAS CHANGED。
  2. 新系统 = authorized_keys 被清空。原来的免密公钥没了,只能先用控制台给的密码把公钥重新装回去。

第一步,清掉本地记的旧指纹(IP 和别名都要清):

for k in 154.201.73.31 154.201.73.81 45.205.31.214 45.205.31.180 45.205.31.10 \
         m1 m2 m3 m4 m5 k8s-cp-1 k8s-cp-2 k8s-cp-3 k8s-w-1 k8s-w-2; do
  ssh-keygen -R "$k"
done

第二步,用密码把公钥装回 5 台。ssh-copy-id 会把本地 ~/.ssh/id_rsa.pub 追加到对端 ~/.ssh/authorized_keys:

ssh-copy-id -o StrictHostKeyChecking=accept-new root@154.201.73.31   # 输入控制台给的密码
# 其余 4 台同理

StrictHostKeyChecking=accept-new:自动接受新机器的指纹并写进 known_hosts;但若是已知机器指纹突然变了仍会拦——比直接关掉校验安全。

第三步——最坑的一步。很多云镜像(含本次用的)默认在 /etc/ssh/sshd_config 里写了 PubkeyAuthentication no,于是公钥装上了也用不了,登录只会回退到密码。现象:

ssh -v root@154.201.73.31 true 2>&1 | grep "can continue"
# debug1: Authentications that can continue: password
#                                            ^^^^^^^^ 只有 password、没有 publickey = 公钥认证被关

服务器上确认并打开:

ssh root@<ip> "sshd -T | grep -i pubkeyauth"       # pubkeyauthentication no  ← 就是它
ssh root@<ip> "sed -i 's/^PubkeyAuthentication no/PubkeyAuthentication yes/' /etc/ssh/sshd_config \
               && systemctl reload ssh"

用 reload 不用 restart:reload 不断开当前连接;restart 会把你正用着的这条 SSH 也踢掉(命令可能在回显前就断,看到 Connection closed)。只能 restart 时用 nohup systemctl restart ssh & 后台重启,再新开连接验证。

验证免密通了:

for h in m1 m2 m3 m4 m5; do echo "$h: $(ssh -o BatchMode=yes $h hostname)"; done

BatchMode=yes 关掉一切交互(不会弹密码框)——能打印出 hostname 就说明纯靠密钥登进去了。

2.1 检查系统 + 核实内网 IP

for h in m1 m2 m3 m4 m5; do
  echo "== $h =="
  ssh -o BatchMode=yes $h '
    hostname
    ip -4 -o addr show | awk "{print \$2, \$4}" | grep "10\\."
    . /etc/os-release && echo "$PRETTY_NAME  $(uname -r)"
    echo "mem=$(free -h | awk "/Mem/{print \$2}") cpu=$(nproc)"
  '
done

真实输出(本集群,5 台同构,只贴 m1):

== m1 ==
ser681858145161
ens17 10.0.24.31/24
Ubuntu 24.04.1 LTS  6.8.0-48-generic
mem=7.8Gi cpu=8

逐项看什么:

字段看什么本集群
hostname重装后是云厂默认名(如 ser6818...),Step 3 才改成 k8s-cp-1待改
10.0.24.x/24最关键:内网 IP 必须和「机器规划」表一致。HAProxy 后端、kubeadm advertiseAddress、etcd peer 全写死这些 IP,变了整套配置都要改31/29/32/28/30 ✓
ens17内网网卡名,后面 VXLAN / 路由走它一致
OS / kernelkernel ≥ 5.4 才能跑 Cilium eBPF,本集群 6.8 没问题6.8 ✓
mem / cpu控制面建议 ≥ 2C2G,本集群 8C8G 宽裕✓

为什么先做这一步:后面所有安装都是远程批量执行。SSH、hostname、内网 IP 任意一个错,kubeadm join 会把错误节点注册进集群,排查代价很高。


3. 写 hostname 和 hosts

只在空机器安装时执行:

ssh root@154.201.73.31 'hostnamectl set-hostname k8s-cp-1'
ssh root@154.201.73.81 'hostnamectl set-hostname k8s-cp-2'
ssh root@45.205.31.214 'hostnamectl set-hostname k8s-cp-3'
ssh root@45.205.31.180 'hostnamectl set-hostname k8s-w-1'
ssh root@45.205.31.10 'hostnamectl set-hostname k8s-w-2'

然后 5 台统一写 /etc/hosts:

for ip in 154.201.73.31 154.201.73.81 45.205.31.214 45.205.31.180 45.205.31.10; do
  ssh root@$ip 'set -eu
    cp -a /etc/hosts /etc/hosts.bak.$(date +%Y%m%d-%H%M%S)
    cp -a /etc/cloud/cloud.cfg /etc/cloud/cloud.cfg.bak.$(date +%Y%m%d-%H%M%S) 2>/dev/null || true

    if grep -q "^manage_etc_hosts:" /etc/cloud/cloud.cfg 2>/dev/null; then
      sed -i "s/^manage_etc_hosts:.*/manage_etc_hosts: false/" /etc/cloud/cloud.cfg
    else
      printf "\nmanage_etc_hosts: false\n" >> /etc/cloud/cloud.cfg
    fi

    cat > /etc/hosts <<HOSTS
127.0.0.1 localhost k8s-api
127.0.1.1 $(hostname)
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

10.0.24.31 k8s-cp-1 m1
10.0.24.29 k8s-cp-2 m2
10.0.24.32 k8s-cp-3 m3
10.0.24.28 k8s-w-1  m4
10.0.24.30 k8s-w-2  m5
HOSTS

    getent hosts k8s-api
  '
done

为什么 k8s-api 指向 127.0.0.1:每台机器本地跑 HAProxy,监听 127.0.0.1:16443,再转发到 3 个 apiserver。这样 kubelet 和 kubectl 都只连本机入口,不依赖外部 LB。


4. 节点基础配置

5 台都执行:

for ip in 154.201.73.31 154.201.73.81 45.205.31.214 45.205.31.180 45.205.31.10; do
  ssh root@$ip 'set -eu
    swapoff -a
    sed -i.bak "/ swap / s/^/#/" /etc/fstab

    cat > /etc/modules-load.d/k8s.conf <<EOF
overlay
br_netfilter
EOF
    modprobe overlay
    modprobe br_netfilter

    cat > /etc/sysctl.d/99-k8s.conf <<EOF
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
    sysctl --system

    apt-get update
    apt-get install -y ca-certificates curl gnupg lsb-release chrony jq ipset ipvsadm conntrack socat ebtables ethtool
    systemctl enable --now chrony
  '
done

为什么:

  • swapoff:kubelet 默认不允许节点开启 swap,避免内存压力时调度判断失真。
  • br_netfilter:让桥接网络经过 iptables/nftables,CNI 和 Service 转发需要。
  • ip_forward:Pod 跨节点、Service 转发都需要 Linux 转发包。
  • chrony:etcd 对时间敏感,时间漂移会导致证书和 Raft 异常。

5. 安装 containerd

5 台都执行:

for ip in 154.201.73.31 154.201.73.81 45.205.31.214 45.205.31.180 45.205.31.10; do
  ssh root@$ip 'set -eu
    install -m 0755 -d /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
      | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
    chmod a+r /etc/apt/keyrings/docker.gpg
    . /etc/os-release
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu ${VERSION_CODENAME} stable" \
      > /etc/apt/sources.list.d/docker.list

    apt-get update
    apt-get install -y containerd.io

    mkdir -p /etc/containerd
    containerd config default > /etc/containerd/config.toml
    sed -i "s/SystemdCgroup = false/SystemdCgroup = true/" /etc/containerd/config.toml
    sed -i "s|config_path = \"\"|config_path = \"/etc/containerd/certs.d\"|" /etc/containerd/config.toml

    mkdir -p /etc/containerd/certs.d/docker.io
    cat > /etc/containerd/certs.d/docker.io/hosts.toml <<EOF
server = "https://docker.io"
[host."https://docker.m.daocloud.io"]
  capabilities = ["pull", "resolve"]
EOF

    systemctl enable --now containerd
    systemctl restart containerd
    systemctl is-active containerd
  '
done

验证(5 台都要 active):

ssh root@<ip> 'systemctl is-active containerd; containerd --version'
# active
# containerd containerd.io 2.2.4 ...

为什么不用 Docker:K8s 1.24 起移除了 dockershim,直接用 containerd 更短、更接近生产。

为什么 SystemdCgroup=true:Ubuntu 24.04 默认用 systemd 管 cgroup,kubelet 和 containerd 的 cgroup driver 必须一致,否则资源限制和 Pod 退出时容易异常。

⚠️ containerd v2 配置格式变了(实测踩坑)

Docker 源现在装的是 containerd v2.2.4,配置文件是 TOML version = 3 新格式,和老教程(v1.x)有三处不同:

项v1.x(老教程)v2.x(现在)
空值写法config_path = ""(双引号)config_path = ''(单引号)
CRI 插件路径io.containerd.grpc.v1.criio.containerd.cri.v1.images
SystemdCgroup 默认false(必须手动改)默认已是 true

后果:上面 sed "s/SystemdCgroup = false/.../" 在 v2 上找不到匹配(默认已是 true,不影响);但 sed 's/config_path = ""/.../' 也匹配不到单引号的 v2 写法,镜像加速 mirror 静默不生效。本 runbook 已改成兼容写法:

# 只替换第一处(registry 那处)的空 config_path,v1 双引号 / v2 单引号都覆盖
sed -i "0,/config_path = ''/s||config_path = \"/etc/containerd/certs.d\"|" /etc/containerd/config.toml
sed -i '0,/config_path = ""/s||config_path = "/etc/containerd/certs.d"|' /etc/containerd/config.toml

验证 mirror 真的写进去了:grep 'config_path = .*certs.d' /etc/containerd/config.toml 应回显 config_path = "/etc/containerd/certs.d"。

另:用 sed 改 TOML 时别在单引号 SSH 参数里再嵌单引号('\'' 极易多写一个引号),结果是 config_path = '/etc/...''',containerd 启动直接报 Failure unmarshaling TOML ... expected newline but got U+0027。要么用 heredoc 把脚本喂给 bash -s,要么像上面用 TOML 合法的双引号写路径值。

本环境实测:5 台能直连 registry.k8s.io(200)和 docker.io(302),所以 daocloud mirror 其实非必需;但配上能在仓库变慢时兜底。国内机器若直连不通,这个 mirror 就是刚需。


6. 安装 kubeadm / kubelet / kubectl

5 台都执行:

for ip in 154.201.73.31 154.201.73.81 45.205.31.214 45.205.31.180 45.205.31.10; do
  ssh root@$ip 'set -eu
    curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key \
      | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
    echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /" \
      > /etc/apt/sources.list.d/kubernetes.list
    apt-get update
    apt-get install -y kubelet kubeadm kubectl
    apt-mark hold kubelet kubeadm kubectl

    cat > /etc/crictl.yaml <<EOF
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
timeout: 10
debug: false
EOF
  '
done

为什么 apt-mark hold:避免系统自动升级 kubelet/kubeadm/kubectl,K8s 版本升级必须按顺序做,不能让 apt 自动滚。


7. 安装 HAProxy 本地 apiserver 入口

5 台都执行:

for ip in 154.201.73.31 154.201.73.81 45.205.31.214 45.205.31.180 45.205.31.10; do
  ssh root@$ip 'set -eu
    apt-get install -y haproxy
    cat > /etc/haproxy/haproxy.cfg <<EOF
global
    log /dev/log local0
    log /dev/log local1 notice
    daemon

defaults
    log global
    mode tcp
    option tcplog
    option dontlognull
    timeout connect 5s
    timeout client  1m
    timeout server  1m

frontend k8s_api
    bind 127.0.0.1:16443
    default_backend k8s_api_backends

backend k8s_api_backends
    option tcp-check
    server cp1 10.0.24.31:6443 check
    server cp2 10.0.24.29:6443 check
    server cp3 10.0.24.32:6443 check
EOF
    systemctl enable haproxy
    systemctl restart haproxy
    ss -tlnp | grep 16443 || true
  '
done

为什么不用公网 IP:apiserver 和 etcd 走内网,减少延迟和暴露面。


8. kubeadm init 第一个控制面

在 k8s-cp-1 执行:

ssh root@154.201.73.31

写 kubeadm 配置:

cat > /root/kubeadm-init.yaml <<'EOF'
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v1.30.14
controlPlaneEndpoint: k8s-api:16443
networking:
  podSubnet: 10.244.0.0/16
  serviceSubnet: 10.96.0.0/12
apiServer:
  certSANs:
  - k8s-api
  - 127.0.0.1
  - 10.0.24.31
  - 10.0.24.29
  - 10.0.24.32
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: 10.0.24.31
  bindPort: 6443
nodeRegistration:
  criSocket: unix:///run/containerd/containerd.sock
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
EOF

kubeadm init --config /root/kubeadm-init.yaml --upload-certs

mkdir -p ~/.kube
cp -f /etc/kubernetes/admin.conf ~/.kube/config
chmod 600 ~/.kube/config

kubectl get nodes

此时节点大概率是 NotReady,因为 CNI 还没装。这是正常状态。

保存 kubeadm 输出里的两条 join 命令:

  • control plane join:给 cp-2/cp-3 用,带 --control-plane --certificate-key。
  • worker join:给 w-1/w-2 用。

8.1 init 输出逐段读懂

kubeadm init 会打出一大段 [phase] 日志,新手容易被吓到。其实它就是按顺序做完这些事,每个方括号是一个阶段:

[preflight]      检查端口/swap/cgroup/镜像,拉控制面镜像
[certs]          生成整套 TLS 证书(ca / apiserver / etcd / front-proxy / sa)
  apiserver serving cert is signed for ... IPs [10.96.0.1 10.0.24.31 127.0.0.1 10.0.24.29 10.0.24.32]
                 ↑ 这行很重要:apiserver 证书签了哪些名字/IP。少了某个 IP,用那个 IP 连 apiserver 会报证书错
[control-plane]  把 apiserver/controller-manager/scheduler 写成静态 Pod manifest 放到 /etc/kubernetes/manifests
[etcd]           本机起一个 stacked etcd(控制面和 etcd 同机)
[kubelet-start]  启动 kubelet,由它拉起上面那些静态 Pod
[wait-control-plane] / [api-check]  等 apiserver 健康(The API server is healthy after 7.5s)
[upload-certs]   把证书塞进 Secret kubeadm-certs,并打印 certificate key(给控制面 join 用,2 小时过期)
[bootstrap-token] 生成 join 用的 token + 配好 RBAC
[addons]         装上 CoreDNS 和 kube-proxy

中间这条 WARNING 可以无视:

W... [endpoint] WARNING: port specified in controlPlaneEndpoint overrides bindPort in the controlplane address

意思是「你在 controlPlaneEndpoint: k8s-api:16443 指定了端口,它会覆盖 bindPort: 6443」——这正是我们想要的(对外走 HAProxy 的 16443,本机 apiserver 仍听 6443),不是错误。

两条 join 命令分别长这样(token / hash / certificate-key 每次 init 都不同):

# 控制面 join(给 cp-2 / cp-3)——多了 --control-plane 和 --certificate-key
kubeadm join k8s-api:16443 --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash> \
  --control-plane --certificate-key <cert-key>

# worker join(给 w-1 / w-2)——只有 token 和 hash
kubeadm join k8s-api:16443 --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash>

配好 kubeconfig 后看一眼:

$ kubectl get nodes
NAME       STATUS     ROLES           AGE   VERSION
k8s-cp-1   NotReady   control-plane   20s   v1.30.14

$ kubectl get pods -n kube-system
coredns-...   0/1   Pending   ← 没 CNI、拿不到 Pod IP,正常
coredns-...   0/1   Pending
etcd-k8s-cp-1                      1/1   Running
kube-apiserver-k8s-cp-1            1/1   Running
kube-controller-manager-k8s-cp-1   1/1   Running
kube-proxy-...                     1/1   Running
kube-scheduler-k8s-cp-1            1/1   Running

✅ 此刻的正确状态:节点 NotReady、coredns Pending,其余控制面组件 Running。这不是故障——CNI(Step 10 的 Cilium)还没装,节点没有 Pod 网络。装完 Cilium 就会转 Ready、coredns 才拿得到 IP。新手最常见的误判就是在这里以为「装坏了」。


9. 加入其余节点

在 cp-2/cp-3 执行 control-plane join。示例:

kubeadm join k8s-api:16443 \
  --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash> \
  --control-plane \
  --certificate-key <cert-key> \
  --cri-socket unix:///run/containerd/containerd.sock

控制面要逐台串行 join,别并发。 每加一个控制面,etcd 就多一个成员,需要重新达成多数派。并发加会让 etcd 仲裁不稳。

join 过程中会刷出这种 WARNING,是正常的、不用管:

{"level":"warn",...,"error":"etcdserver: can only promote a learner member which is in sync with leader"}

新 etcd 成员先以 learner(学习者) 身份加入、只同步数据不投票;等它追上 leader,kubeadm 再把它提升为正式成员。上面的 warning 就是「还在追、稍等」的重试日志。最后看到 A new etcd member was added to the local/stacked etcd cluster 就成了。

在 w-1/w-2 执行 worker join:

kubeadm join k8s-api:16443 \
  --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash> \
  --cri-socket unix:///run/containerd/containerd.sock

如果 token 过期,在 cp-1 重新生成:

kubeadm token create --print-join-command
kubeadm init phase upload-certs --upload-certs

验收:

kubectl get nodes -o wide

真实输出(5 台都 join 完、Cilium 还没装):

NAME       STATUS     ROLES           AGE     VERSION    INTERNAL-IP   EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION     CONTAINER-RUNTIME
k8s-cp-1   NotReady   control-plane   4m2s    v1.30.14   10.0.24.31    <none>        Ubuntu 24.04.1 LTS   6.8.0-48-generic   containerd://2.2.4
k8s-cp-2   NotReady   control-plane   2m46s   v1.30.14   10.0.24.29    <none>        Ubuntu 24.04.1 LTS   6.8.0-48-generic   containerd://2.2.4
k8s-cp-3   NotReady   control-plane   46s     v1.30.14   10.0.24.32    <none>        ...
k8s-w-1    NotReady   <none>          19s     v1.30.14   10.0.24.28    <none>        ...
k8s-w-2    NotReady   <none>          13s     v1.30.14   10.0.24.30    <none>        ...

kubectl get nodes -o wide 逐列说明(新手最该背的一张表):

列含义怎么判断
NAME节点名 = Step 3 设的 hostname应是 k8s-cp-1…k8s-w-2,不是云厂默认名
STATUSReady / NotReady没装 CNI 前全 NotReady 正常;装完 Cilium 应全 Ready
ROLEScontrol-plane 或 <none>worker 显示 <none> 是正常的(K8s 没有内置 worker role 标签)
AGE节点 join 进来多久cp-1 最老(先 init),worker 最新
VERSIONkubelet 版本必须一致,本集群全 v1.30.14
INTERNAL-IP节点内网 IP必须和机器规划表一致;apiserver / etcd / VXLAN 都用它
EXTERNAL-IP外网 IP(kubelet 探测到的)<none> 正常,集群内部通信不用它
CONTAINER-RUNTIME运行时及版本containerd://2.2.4,确认不是 docker

✅ 验证点:5 台全部出现(少一台说明那台 join 失败)+ VERSION 一致 + INTERNAL-IP 对得上规划表。NotReady 此刻是对的,等 Cilium 装完恢复(见 Step 10 / Cilium Runbook)。

⚠️ 如果 token 过期(默认 24h)或想再加节点,在 cp-1 重新生成:

kubeadm token create --print-join-command          # 打印 worker join(含新 token+hash)
kubeadm init phase upload-certs --upload-certs      # 重新上传证书,打印新 certificate-key(控制面 join 要)

10. 安装平台组件

按顺序安装,具体细节看组件 Runbook:

后续按组件 Runbook 执行:

  • CNI 网络:Cilium 网络 Runbook
  • 存储:Longhorn 存储 Runbook
  • 监控日志:监控日志 Runbook
  • 镜像仓库、Git、CI、CD:CI / GitOps Runbook
  • 安全和准入策略:安全准入 Runbook

顺序不能乱:

  1. Cilium 必须最先装,否则节点一直 NotReady,Pod 没网络。
  2. Longhorn 依赖 K8s 网络正常。
  3. 监控日志依赖 StorageClass 提供 PVC。
  4. Harbor/Gitea/Jenkins/ArgoCD 也依赖 PVC 和网络。

11. 最终验收

kubectl get nodes -o wide

kubectl get pods -A \
  --field-selector=status.phase!=Running,status.phase!=Succeeded \
  -o wide

helm list -A

cilium status --wait=false

kubectl get sc

kubectl get pvc -A

kubectl get svc -A | grep NodePort

期望:

  • 5 个节点都是 Ready。
  • 除演练 Pod 外,没有业务组件 CrashLoop。
  • longhorn 是默认 StorageClass。
  • Cilium Desired: 5, Ready: 5/5。
  • Grafana、Harbor、Jenkins、ArgoCD、Longhorn 都有 NodePort。
在 GitHub 上编辑此页
Prev
Runbook 总览:从零部署、查看、调试
Next
核心 K8s Runbook:apiserver / etcd / kubelet / containerd / HAProxy