Kubernetes 二进制部署实战¶
作者:JiangChong | 撰写时间:2026年08月
本文采用二进制逐组件部署,目的是理解每个组件的作用和配置。读完本文可对照 Kubernetes 核心架构 和 Kubernetes Pod 与 Deployment 实战 补充理论细节。相关概念亦可参考 Vertica on Kubernetes 部署指南 的 kind 环境快速体验。
三种部署方式对比
| 维度 | Kind | 二进制部署(本文方式) | kubeadm |
|---|---|---|---|
| 部署命令 | kind create cluster 一条命令 |
手动下载二进制 → 写 systemd → 逐组件启动 | kubeadm init + kubeadm join |
| 运行载体 | Docker 容器(每「节点」是一个容器) | 裸机 / 虚拟机(真实 OS 进程) | 裸机 / 虚拟机(真实 OS 进程) |
| 证书管理 | 自动生成(自签证书,本地测试 OK) | 手动用 cfssl 生成 ~7 套证书,配置到每个组件 | kubeadm init 自动生成,certs renew 统一续期 |
| 配置文件 | 全自动,零配置 | 手工编写所有 conf / kubeconfig / service 文件 | 自动生成 /etc/kubernetes/manifests/ 下静态 Pod |
| 组件运行 | Docker 容器内,docker ps 看到节点容器 |
systemd 管理,每个组件是独立 Linux 进程 | 静态 Pod,kubelet 管理(kube-system ns) |
| 升级 | kind delete cluster → 新版镜像重建 |
停服务 → 换二进制 → 改配置 → 重启 | kubeadm upgrade plan → upgrade apply |
| 加节点 | YAML 配置加 role: worker,重建集群 |
scp 二进制 + 证书 + 手写 systemd service | kubeadm join <VIP>:6443 --token ... |
| 学习价值 | 🔻 低 — 一键启动,适合快速跑实验 | 🔺 高 — 知道每个参数作用和组件通信细节 | 🔸 中 — 理解 K8s 部署的标准抽象 |
| 适用场景 | 本地开发、CI/CD、学习 K8s 概念 | 学习原理、离线/安全加固、定制化部署 | 生产环境标准方式(CNCF 官方推荐) |
三者关系:Kind 让你「用 K8s」— 5 分钟起集群,专注学 Pod、Deployment、HPA 这些上层概念(本系列 01-06 篇全程 Kind)。kubeadm 让你「搭 K8s」— 标准化地部署生产集群。二进制部署让你「懂 K8s」— 把证书怎么签、kubeconfig 怎么配、组件怎么起全部手工操作一遍;理解了本文之后回头看 kubeadm,会发现它只是「把手工活写成代码」的自动化工具。
一、Kubernetes 简介¶
1.1 Kubernetes 是什么¶
Kubernetes(K8s)是 Google 开源的容器集群管理系统,是一站式的分布式系统开发和支撑平台,对现有编程语言、编程框架、中间件没有任何侵入性。
K8s 提供了完善的集群管理能力,包括多层次安全防护和准入机制、多租户应用支撑能力、透明的服务注册和服务发现、内建智能负载均衡、强大的故障发现和自我修复能力、服务滚动升级和在线扩容、可扩展的资源自动调度、多粒度的资源配额管理。
Kubernetes 官方文档:https://kubernetes.io/zh/
1.2 Kubernetes 特性¶
- 自我修复:节点故障时自动重启失败的容器,保证预期的副本数量;健康检查失败的容器在未恢复前不接流量。
- 弹性伸缩:基于 CPU/内存等指标自动扩缩容,业务高峰期自动增加实例,低峰期回收资源。
- 自动部署和回滚:采用滚动更新策略逐步替换 Pod,出现问题自动回滚,确保升级不中断业务。
- 服务发现和负载均衡:为多个容器提供统一访问入口(ClusterIP + DNS 名称),自动负载均衡所有后端 Pod。
- 机密和配置管理:通过 Secret 和 ConfigMap 管理敏感数据和配置,不暴露在镜像中。
- 存储编排:支持挂载本地存储、公有云存储、网络存储(NFS/Ceph/GlusterFS 等)。
- 批处理:提供 Job 和 CronJob 资源,满足批量处理和定时任务场景。
二、集群架构与组件¶
Kubernetes 集群由 Master 节点(控制平面)和 Node 节点(工作负载)组成。
2.1 Master(控制平面)¶
Master 负责整个集群的管理和控制,生产环境建议部署多个 Master 保证高可用。核心组件:
- kube-apiserver:集群统一入口,所有资源操作的 REST API 网关,唯一与 etcd 直接交互的组件。
- kube-controller-manager:所有资源对象的自动化控制中心,每种资源对应一个控制器(Deployment Controller、Node Controller 等),遵循"观察 → 比较 → 执行"的调谐循环(Reconciliation Loop)。
- kube-scheduler:根据调度算法为新创建的 Pod 选择最优 Node 节点。
- etcd:分布式一致的 key-value 存储,保存集群所有状态数据,基于 Raft 共识算法。
2.2 Node(工作节点)¶
Node 是实际运行业务负载的节点,每个 Node 运行以下组件:
- kubelet:Master 在 Node 上的 Agent,管理本机 Pod 的生命周期,负责容器创建、启停、健康检查。
- kube-proxy:实现 Service 通信的网络代理,维护 iptables/IPVS 规则,提供四层负载均衡。
- 容器运行时:通过 CRI(Container Runtime Interface)与 kubelet 解耦。当前主流是 containerd,Docker 已弃用(K8s v1.24+ 移除 dockershim)。
Node 可动态加入集群,kubelet 定期向 Master 上报节点状态(OS、容器运行时版本、CPU/内存、运行中的 Pod 等)。超时未上报则节点标记为 NotReady,触发 Pod 重新调度。
三、核心概念¶
1、Pod
Pod 是 K8s 最小部署单元,是一组共享网络和存储的容器集合。每个 Pod 包含一个 Pause 根容器(提供网络命名空间)和一个或多个业务容器。Pod 内容器共享 Pause 容器的 IP 和挂载的 Volume。
2、Label
标签,key=value 键值对,用于关联、筛选和分组资源对象。通过 Label Selector(基于等式或集合匹配)实现多维度资源分组。
3、ReplicaSet
确保指定数量的 Pod 副本始终运行,过少则创建,过多则终止。通常不直接使用 ReplicaSet,而是通过 Deployment 管理。
注意:老版本的 RC(ReplicationController)已被 ReplicaSet 取代,后者支持集合选择器(
in、notin),功能更强。
4、Deployment
为 Pod 和 ReplicaSet 提供声明式更新,描述目标状态后 Deployment Controller 自动将实际状态调整到目标状态,支持滚动更新、回滚、暂停/恢复。
5、Horizontal Pod Autoscaler(HPA)
HPA 通过追踪 Pod 的 CPU/内存等指标,自动调整 Deployment/ReplicaSet 的副本数量,支持基于自定义指标的弹性伸缩。
6、Service
定义一组 Pod 的访问策略。通过 Label Selector 关联后端 Pod,自动分配全局唯一的 ClusterIP。服务发现通过 Service Name → ClusterIP 的 DNS 映射实现。
7、Namespace
命名空间,用于多租户资源隔离。将资源对象"分配"到不同 Namespace 中,形成逻辑上的分组。配合 ResourceQuota 限定每个租户可占用的 CPU、内存等资源。
四、集群搭建 —— 平台规划¶
4.1 生产环境 K8s 平台规划¶
K8s 环境分为单 Master 和多 Master 两种架构。开发测试可用单 Master,生产环境必须多 Master 保证高可用。
4.1.1 单 Master 集群架构¶
一台 Master + 多台 Node + 多节点 Etcd 集群。Etcd 可与 Master 同机部署,只要网络可达。

4.1.2 多 Master 集群架构¶
在单 Master 基础上增加多个 Master 节点,前面加一层 LB 负载均衡器。Node 连接 LB 的 VIP,LB 分发到各 Master 的 API Server。

4.1.3 集群规划¶
示例环境使用 6 台阿里云 ecs.e-c1m2.large 服务器,规划如下:
| 序号 | 角色 | 私网IP | 主机名 | 组件 |
|---|---|---|---|---|
| 1 | k8s-master-1 | 172.20.65.69 | k8s-master-1sudo hostnamectl set-hostname k8s-master-1 |
kube-apiserver kube-controller-manager kube-scheduler etcd |
| 2 | k8s-master-2 | 172.20.65.73 | k8s-master-2sudo hostnamectl set-hostname k8s-master-2 |
kube-apiserver kube-controller-manager kube-scheduler |
| 3 | k8s-node-1 | 172.20.65.70 | k8s-node-1sudo hostnamectl set-hostname k8s-node-1 |
kubelet kube-proxy containerd etcd |
| 4 | k8s-node-2 | 172.20.65.71 | k8s-node-2sudo hostnamectl set-hostname k8s-node-2 |
kubelet kube-proxy containerd etcd |
| 5 | Load Balancer(Master) | 172.20.65.74 | k8s-lb-mastersudo hostnamectl set-hostname k8s-lb-master |
Nginx L4 |
| 6 | Load Balancer(Backup) | 172.20.65.72 | k8s-lb-backupsudo hostnamectl set-hostname k8s-lb-backup |
Nginx L4 |
生产环境建议:Master 至少 2 台、LB 主备各 1 台、Node 至少 2 台。Etcd 资源充裕时推荐独立部署。
4.1.4 服务器硬件配置推荐¶
| 环境 | CPU | 内存 | 系统盘 | 数据盘 | 操作系统 |
|---|---|---|---|---|---|
| 测试 | 2 核+ | 4GB+ | 40GB | 100GB | |
| 生产 | 8 核+ | 32GB+ | 100GB SSD | 500GB+ SSD | |
| 本测试环境 | 2 核 | 4 GiB | 40GiB | 无 | Debian 13.6 64位 |
K8s v1.36 对硬件要求与 v1.16 基本一致,但 etcd v3.5 建议使用 NVMe SSD 以降低延迟。
4.2 操作系统初始化¶
以下操作在所有节点执行(CentOS 与 Debian 命令有差异,按你的系统选择对应命令)。
4.2.1 关闭防火墙¶
# Debian / Ubuntu(默认没有 firewalld;如有 ufw 则关闭)
systemctl stop ufw 2>/dev/null
systemctl disable ufw 2>/dev/null
Debian 默认不装防火墙软件,阿里云安全组已做网络隔离,此步通常直接跳过即可。
4.2.2 关闭 SELinux¶
Debian / Ubuntu 默认未安装 SELinux(使用 AppArmor,与 K8s 兼容,无需关闭),此步跳过。
4.2.3 关闭 swap¶
# CentOS / Debian 命令相同
swapoff -a
# 编辑 /etc/fstab,注释 swap 行:
# sed -i 's/^\(.*[[:space:]]swap[[:space:]].*\)/#\1/' /etc/fstab
4.2.4 添加 hosts(把每台机器的私网 IP 对应主机名写全,各节点保持一致)¶
cat >> /etc/hosts <<EOF
172.20.65.69 k8s-master-1
172.20.65.73 k8s-master-2
172.20.65.70 k8s-node-1
172.20.65.71 k8s-node-2
172.20.65.74 k8s-lb-master
172.20.65.72 k8s-lb-backup
EOF
按 4.1.3 规划表填入实际私网 IP;同时用
hostnamectl set-hostname <主机名>把每台机器主机名改成对应名称(⚠️ 阿里云注意:cloud-init 默认会用实例名称覆盖主机名,需在/etc/cloud/cloud.cfg设preserve_hostname: true;主机名只能小写字母/数字/连字符)。
4.2.5 同步系统时间¶
# Debian / Ubuntu(默认 systemd-timesyncd,一条命令开启)
timedatectl set-ntp true
timedatectl status # 确认 "System clock synchronized: yes"
时间不一致会导致自签证书校验失败。
4.2.6 加载内核模块¶
# CentOS / Debian 命令相同
modprobe overlay # overlayfs 文件系统(容器镜像分层存储)
modprobe br_netfilter # 加载 br_netfilter 模块(下面的 bridge-nf 参数依赖它,不加载会报 No such file)
# 开机自动加载(⚠️ 必须做:重启后模块丢失会导致 Flannel 启动失败:
# "Failed to check br_netfilter: stat /proc/sys/net/bridge/bridge-nf-call-iptables: no such file or directory")
cat > /etc/modules-load.d/k8s-net.conf <<EOF
overlay
br_netfilter
EOF
cat > /etc/sysctl.d/k8s.conf <<EOF
# 让经过 Linux 网桥的流量也走 iptables(kube-proxy 的 NAT/NetworkPolicy 规则才能作用于容器间流量)
net.bridge.bridge-nf-call-iptables = 1
# 同上,IPv6 版本(单 IPv4 集群不生效,IPv6/双栈集群需要)
net.bridge.bridge-nf-call-ip6tables = 1
# 开启 IP 转发,宿主机才能当路由器转发跨节点 Pod 流量
net.ipv4.ip_forward = 1
EOF
sysctl --system # 立即加载 /etc/sysctl.d/ 下所有配置
五、部署 Etcd 集群¶
5.1 自签证书概述¶
K8s 各组件间通过 CA 签名的双向数字证书进行认证,安全性最高。
⚠️ 执行节点:本节及后续所有证书生成命令(5.1 ~ 5.x 全部)只在
k8s-master-1节点上执行,生成后再 scp 分发到各节点。若在其他节点误执行,证书与主机名/路径不匹配,整批作废重来。工具变更:原 cfssl 下载源
pkg.cfssl.org已失效,现在从 GitHub Releases 下载。
# 下载 cfssl 工具(请检查最新版本)
curl -L https://github.com/cloudflare/cfssl/releases/download/v1.6.5/cfssl_1.6.5_linux_amd64 -o /usr/local/bin/cfssl
curl -L https://github.com/cloudflare/cfssl/releases/download/v1.6.5/cfssljson_1.6.5_linux_amd64 -o /usr/local/bin/cfssljson
chmod +x /usr/local/bin/cfssl /usr/local/bin/cfssljson
5.2 生成 Etcd SSL 证书¶
⚠️ 以下命令继续在
k8s-master-1节点执行。
mkdir -p /k8s/etcd/{ssl,cfg,bin}
mkdir -p /k8s/data/default.etcd # etcd 数据目录(etcd.service 的 WorkingDirectory 指向这里,systemd 不会自动创建,必须先存在)
cd /k8s/etcd/ssl
5.2.1 创建 CA 配置文件 ca-config.json¶
cat > ca-config.json <<EOF
{
"signing": {
"default": { "expiry": "87600h" },
"profiles": {
"etcd": {
"usages": ["signing", "key encipherment", "server auth", "client auth"],
"expiry": "87600h"
}
}
}
}
EOF
5.2.2 创建 CA 证书签名请求 ca-csr.json¶
cat > ca-csr.json <<EOF
{
"CN": "etcd",
"key": { "algo": "rsa", "size": 2048 },
"names": [
{ "C": "CN", "ST": "ChongQing", "L": "ChongQing", "O": "etcd", "OU": "System" }
],
"ca": { "expiry": "87600h" }
}
EOF
5.2.3 生成 CA 证书和私钥¶
cfssl gencert -initca ca-csr.json | cfssljson -bare ca
# 生成: ca.pem(CA 数字证书)、ca-key.pem(CA 私钥)
# 2026/08/04 20:41:49 [INFO] generating a new CA key and certificate from CSR
# 2026/08/04 20:41:49 [INFO] generate received request
# 2026/08/04 20:41:49 [INFO] received CSR
# 2026/08/04 20:41:49 [INFO] generating key: rsa-2048
# 2026/08/04 20:41:49 [INFO] encoded CSR
# 2026/08/04 20:41:49 [INFO] signed certificate with serial number 392906971611207528500823464016123976081969507013
5.2.4 创建 etcd 证书签名请求 etcd-csr.json¶
这里为规划的 3 台 etcd 节点(k8s-master-1, k8s-node-1, k8s-node-2 )创建证书
cat > etcd-csr.json <<EOF
{
"CN": "etcd",
"hosts": [
"172.20.65.69",
"172.20.65.70",
"172.20.65.71"
],
"key": { "algo": "rsa", "size": 2048 },
"names": [
{ "C": "CN", "ST": "ChongQing", "L": "ChongQing", "O": "etcd", "OU": "System" }
]
}
EOF
5.2.5 为 etcd 生成证书和私钥¶
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=etcd etcd-csr.json | cfssljson -bare etcd
# 生成: etcd.pem、etcd-key.pem
# 2026/08/04 20:42:18 [INFO] generate received request
# 2026/08/04 20:42:18 [INFO] received CSR
# 2026/08/04 20:42:18 [INFO] generating key: rsa-2048
# 2026/08/04 20:42:18 [INFO] encoded CSR
# 2026/08/04 20:42:18 [INFO] signed certificate with serial number 647278293123395576839710129355066574699459760641
5.3 Etcd 集群部署¶
etcd v3.5 相比 v3.2 有较大变化:
ETCDCTL_API=3现在是默认值;--listen-peer-urls等启动参数保持不变,但新增了--experimental-*等选项。详见 etcd v3.5 文档。
5.3.1 下载 etcd¶
cd /k8s/etcd
wget https://github.com/etcd-io/etcd/releases/download/v3.5.17/etcd-v3.5.17-linux-amd64.tar.gz
tar zxf etcd-v3.5.17-linux-amd64.tar.gz
cp etcd-v3.5.17-linux-amd64/{etcd,etcdctl} /k8s/etcd/bin
rm -rf etcd-v3.5.17-linux-amd64*
5.3.2 创建 etcd 配置文件 etcd.conf¶
cat > /k8s/etcd/cfg/etcd.conf <<EOF
# [member]
ETCD_NAME=etcd-1
ETCD_DATA_DIR=/k8s/data/default.etcd
ETCD_LISTEN_PEER_URLS=https://172.20.65.69:2380
# ⚠️ EnvironmentFile 不支持行内注释(# 必须独立成行,否则整段文字会进入变量值导致 etcd 启动失败)
# 追加本机地址,方便本机 etcdctl 访问
ETCD_LISTEN_CLIENT_URLS=https://172.20.65.69:2379,http://127.0.0.1:2379
# [cluster]
ETCD_INITIAL_ADVERTISE_PEER_URLS=https://172.20.65.69:2380
ETCD_ADVERTISE_CLIENT_URLS=https://172.20.65.69:2379
ETCD_INITIAL_CLUSTER=etcd-1=https://172.20.65.69:2380,etcd-2=https://172.20.65.70:2380,etcd-3=https://172.20.65.71:2380
ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster
ETCD_INITIAL_CLUSTER_STATE=new
# [security]
ETCD_CERT_FILE=/k8s/etcd/ssl/etcd.pem
ETCD_KEY_FILE=/k8s/etcd/ssl/etcd-key.pem
ETCD_TRUSTED_CA_FILE=/k8s/etcd/ssl/ca.pem
ETCD_PEER_CERT_FILE=/k8s/etcd/ssl/etcd.pem
ETCD_PEER_KEY_FILE=/k8s/etcd/ssl/etcd-key.pem
ETCD_PEER_TRUSTED_CA_FILE=/k8s/etcd/ssl/ca.pem
EOF
参数说明:
| 参数 | 值 | 说明 |
|---|---|---|
| ETCD_NAME | etcd-1 |
节点在集群中的唯一名称,每个节点一个名称 |
| ETCD_LISTEN_PEER_URLS | https://172.20.65.69:2380 |
集群内部通讯地址,「我在本机哪个地址监听」→ 每台监听自己的 IP |
| ETCD_LISTEN_CLIENT_URLS | https://172.20.65.69:2379,http://127.0.0.1:2379 |
客户端访问地址,同上,客户端访问的监听地址 → 自己的 IP;127.0.0.1 供本机 etcdctl 调试 |
| ETCD_INITIAL_ADVERTISE_PEER_URLS | https://172.20.65.69:2380 |
「告诉其他节点:来找我请走这个地址」→ 自己的 IP |
| ETCD_ADVERTISE_CLIENT_URLS | https://172.20.65.69:2379 |
「告诉客户端:来找我请走这个地址」→ 自己的 IP |
| ETCD_INITIAL_CLUSTER | etcd-1=https://172.20.65.69:2380,etcd-2=https://172.20.65.70:2380,etcd-3=https://172.20.65.71:2380 |
所有集群节点地址,「这个集群有哪些成员」→ 三台统一,不含本机立场 |
| ETCD_INITIAL_CLUSTER_TOKEN | etcd-cluster |
集群令牌,简单认证 |
5.3.3 创建 etcd 服务 etcd.service¶
cat > /k8s/etcd/etcd.service <<'EOF'
[Unit]
Description=Etcd Server
After=network.target
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
EnvironmentFile=/k8s/etcd/cfg/etcd.conf
# ⚠️ WorkingDirectory= 不支持环境变量展开,必须写绝对路径(写成 ${ETCD_DATA_DIR} 会报 fatal error)
WorkingDirectory=/k8s/data/default.etcd
# ⚠️ ExecStart 用裸命令:etcd 自动把 ETCD_* 环境变量映射为对应参数(ETCD_NAME→--name 等)。
# 不能写成 --name=${ETCD_NAME} 这种 flag+环境变量双传——etcd 3.5+ 检测到冲突会拒绝启动
# (conflicting environment variable is shadowed by corresponding command-line flag)
ExecStart=/k8s/etcd/bin/etcd
Restart=on-failure
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
5.3.4 分发到其它节点并修改配置¶
# ⚠️ 必须在 etcd 首次启动前分发(否则 master-1 的 /k8s/data 数据目录会被一起拷过去,
# node 上 etcd 启动时可能与 ETCD_INITIAL_CLUSTER_STATE=new 冲突)
scp -r /k8s root@k8s-node-1:/
scp -r /k8s root@k8s-node-2:/
在 k8s-node-1 修改 /k8s/etcd/cfg/etcd.conf:
ETCD_NAME=etcd-2- IP 改为
172.20.65.70
在 k8s-node-2 修改 /k8s/etcd/cfg/etcd.conf:
ETCD_NAME=etcd-3- IP 改为
172.20.65.71
5.3.5 启动 etcd¶
所有三个节点执行:
cp /k8s/etcd/etcd.service /usr/lib/systemd/system/
systemctl daemon-reload
systemctl start etcd
systemctl enable etcd
5.3.6 查看集群状态¶
/k8s/etcd/bin/etcdctl \
--cacert=/k8s/etcd/ssl/ca.pem \
--cert=/k8s/etcd/ssl/etcd.pem \
--key=/k8s/etcd/ssl/etcd-key.pem \
--endpoints=https://172.20.65.69:2379,https://172.20.65.70:2379,https://172.20.65.71:2379 \
endpoint status --write-out=table
# +---------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
# | ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS |
# +---------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
# | https://172.20.65.69:2379 | 7bd26911face9c15 | 3.5.17 | 20 kB | true | false | 2 | 9 | 9 | |
# | https://172.20.65.70:2379 | 4cdca7230e292531 | 3.5.17 | 20 kB | false | false | 2 | 9 | 9 | |
# | https://172.20.65.71:2379 | cc38b303c8f9724 | 3.5.17 | 20 kB | false | false | 2 | 9 | 9 | |
# +---------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
v3.5 变化:
cluster-health命令已废弃,改用endpoint status和endpoint health。
5.4 Etcd 部署常见报错速查¶
以下为本部署流程实测踩过的坑(etcd 3.5.x + systemd 256 / Debian 13):
错误检查方法:
| 报错特征 | 根因 | 修复 |
|---|---|---|
WorkingDirectory= path is not absolute: ${ETCD_DATA_DIR} + bad unit file setting |
WorkingDirectory= 不支持环境变量展开,${VAR} 被当字面量相对路径(fatal error) |
unit 文件改为绝对路径字面量:WorkingDirectory=/k8s/data/default.etcd |
Failed at step CHDIR ... No such file or directory(status=200/CHDIR) |
WorkingDirectory 指向的目录不存在,systemd 不自动创建 | mkdir -p /k8s/data/default.etcd(三台都要) |
conflicting environment variable is shadowed by corresponding command-line flag |
etcd 3.5+ 安全检查:同一参数同时通过 --flag 和环境变量传入则拒绝启动 |
ExecStart=/k8s/etcd/bin/etcd 裸命令,配置全部由 EnvironmentFile 的 ETCD_* 变量承载 |
invalid value "...:2379,,http://..." for ETCD_ADVERTISE_CLIENT_URLS |
配置中多了逗号产生空 URL;或 ADVERTISE 误加 127.0.0.1 |
修正为 ETCD_ADVERTISE_CLIENT_URLS=https://<本机IP>:2379(ADVERTISE 只写本机可达 IP) |
启动失败且值里带着 # 注释文字(如 invalid port ":2379 ") |
EnvironmentFile 不支持行内注释,# 后的文字进入变量值 |
注释独立成行(行首 #),不要写在行尾 |
单节点 start 报 timeout was exceeded / 日志 failed to publish local member |
3 节点集群只有 1 个节点,Raft 需要 2 票选主——正常现象 | 无需处理;等另外两台起来自动完成选主,被 systemd 超时杀掉就再 systemctl start etcd |
三条关键规则:
- systemd 的
ExecStart=支持${VAR}展开,但WorkingDirectory=不支持——后者必须写绝对路径 - etcd 3.5+ 禁止同一配置同时走 flag 和环境变量 → unit 文件用
EnvironmentFile全量传参,ExecStart写裸命令 ADVERTISE系列只写本机可达 IP(会写入集群成员表);127.0.0.1只加在LISTEN系列(本机 etcdctl 调试用)
六、部署 Master 组件¶
6.1 生成 K8s 集群证书¶
⚠️ 执行节点:本节所有命令(创建目录、生成全部证书)都在
k8s-master-1节点执行,与 5.x 证书生成位置一致(统一生成,之后 scp 分发到各节点)。/k8s/kubernetes/ssl/下的ca-key.pem是集群根私钥,务必只在 master-1 保留。
6.1.1 创建 CA 配置文件 ca-config.json¶
cat > ca-config.json <<EOF
{
"signing": {
"default": { "expiry": "87600h" },
"profiles": {
"kubernetes": {
"usages": ["signing", "key encipherment", "server auth", "client auth"],
"expiry": "87600h"
}
}
}
}
EOF
6.1.2 创建 CA 证书签名请求 ca-csr.json¶
cat > ca-csr.json <<EOF
{
"CN": "kubernetes",
"key": { "algo": "rsa", "size": 2048 },
"names": [
{ "C": "CN", "ST": "ChongQing", "L": "ChongQing", "O": "kubernetes", "OU": "System" }
],
"ca": { "expiry": "87600h" }
}
EOF
6.1.3 生成 CA 证书¶
cfssl gencert -initca ca-csr.json | cfssljson -bare ca
# 2026/08/04 22:32:35 [INFO] generating a new CA key and certificate from CSR
# 2026/08/04 22:32:35 [INFO] generate received request
# 2026/08/04 22:32:35 [INFO] received CSR
# 2026/08/04 22:32:35 [INFO] generating key: rsa-2048
# 2026/08/04 22:32:35 [INFO] encoded CSR
# 2026/08/04 22:32:35 [INFO] signed certificate with serial number 403275046868860326040798682971515563653652515233
root@k8s-master-1:/k8s/kubernetes/ssl# ll -trh ca*
# -rw-r--r-- 1 root root 224 Aug 4 22:32 ca-config.json
# -rw-r--r-- 1 root root 207 Aug 4 22:32 ca-csr.json
# -rw-r--r-- 1 root root 1.4K Aug 4 22:32 ca.pem
# -rw------- 1 root root 1.7K Aug 4 22:32 ca-key.pem
# -rw-r--r-- 1 root root 1.1K Aug 4 22:32 ca.csr
6.1.4 创建 API Server 证书签名请求 kubernetes-csr.json¶
cat > kubernetes-csr.json <<EOF
{
"CN": "kubernetes",
"hosts": [
"127.0.0.1",
"10.0.0.1",
"172.20.65.69",
"172.20.65.73",
"172.20.65.74",
"kubernetes",
"kubernetes.default",
"kubernetes.default.svc",
"kubernetes.default.svc.cluster",
"kubernetes.default.svc.cluster.local"
],
"key": { "algo": "rsa", "size": 2048 },
"names": [
{ "C": "CN", "ST": "ChongQing", "L": "ChongQing", "O": "kubernetes", "OU": "System" }
]
}
EOF
说明:
hosts是证书的 SAN(Subject Alternative Name,主题备用名称)——声明「这张证书对哪些地址有效」。TLS 握手时,客户端连接的是什么地址,证书 SAN 里就必须有这个地址,否则报certificate is valid for X, not Y。所以hosts需包含所有访问 apiserver 的入口地址:
| hosts 项 | 谁通过这个地址访问 apiserver | 漏了会怎样 |
|---|---|---|
127.0.0.1 |
本机回环访问 apiserver(调试/健康检查等场景;⚠️ 组件的 kubeconfig 不能写 127.0.0.1——apiserver 不回环监听,见 6.3) | 本机回环访问时握手失败 |
10.0.0.1 |
集群内所有 Pod——Service 网段 10.0.0.0/24 的第一个 IP,固定分配给 kubernetes Service |
Pod 调 K8s API 全部 TLS 失败 |
172.20.65.69 / 172.20.65.73 |
运维/外部客户端直连各 Master | 从对应 IP 连接握手失败 |
172.20.65.74 |
本测试方案:kubelet 等通过 nginx(LB 单点)连接。生产改用 VIP/SLB 时,这一项要换成 VIP/SLB 的 IP(不是 74/72 任一物理 IP) | 最容易漏:客户端校验的是「我实际连接的地址」(LB 入口),不是 LB 后端转发的 Master IP |
kubernetes ~ kubernetes.default.svc.cluster.local |
集群内用 Service 域名访问 | 域名访问时校验失败 |
无需包含 etcd 集群 IP:apiserver 连接 etcd 时是客户端身份,用的是 5.2 节单独生成的
etcd.pem(--etcd-certfile指定),与这张 apiserver 服务端证书无关。
6.1.5 生成 API Server 证书¶
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kubernetes-csr.json | cfssljson -bare kubernetes
# 2026/08/04 23:00:49 [INFO] generate received request
# 2026/08/04 23:00:49 [INFO] received CSR
# 2026/08/04 23:00:49 [INFO] generating key: rsa-2048
# 2026/08/04 23:00:49 [INFO] encoded CSR
# 2026/08/04 23:00:49 [INFO] signed certificate with serial number 640805702861693563708668379530681121756732833732
root@k8s-master-1:/k8s/kubernetes/ssl# ll -trh kubernetes*
# -rw-r--r-- 1 root root 475 Aug 4 22:33 kubernetes-csr.json
# -rw-r--r-- 1 root root 1.6K Aug 4 23:00 kubernetes.pem
# -rw------- 1 root root 1.7K Aug 4 23:00 kubernetes-key.pem
# -rw-r--r-- 1 root root 1.3K Aug 4 23:00 kubernetes.csr
6.1.6 创建 Admin 证书(用于 kubectl 管理集群)¶
cat > admin-csr.json <<EOF
{
"CN": "admin",
"hosts": [],
"key": { "algo": "rsa", "size": 2048 },
"names": [
{ "C": "CN", "ST": "ChongQing", "L": "ChongQing", "O": "system:masters", "OU": "System" }
]
}
EOF
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes admin-csr.json | cfssljson -bare admin
# 2026/08/04 23:01:26 [INFO] generate received request
# 2026/08/04 23:01:26 [INFO] received CSR
# 2026/08/04 23:01:26 [INFO] generating key: rsa-2048
# 2026/08/04 23:01:26 [INFO] encoded CSR
# 2026/08/04 23:01:26 [INFO] signed certificate with serial number 285173617724515345305543385352004549506507269116
# 2026/08/04 23:01:26 [WARNING] This certificate lacks a "hosts" field. This makes it unsuitable for
# websites. For more information see the Baseline Requirements for the Issuance and Management
# of Publicly-Trusted Certificates, v.1.1.6, from the CA/Browser Forum (https://cabforum.org);
# specifically, section 10.2.3 ("Information Requirements").
root@k8s-master-1:/k8s/kubernetes/ssl# ll -trh admin*
# -rw-r--r-- 1 root root 201 Aug 4 23:01 admin-csr.json
# -rw-r--r-- 1 root root 1.4K Aug 4 23:01 admin.pem
# -rw------- 1 root root 1.7K Aug 4 23:01 admin-key.pem
# -rw-r--r-- 1 root root 1013 Aug 4 23:01 admin.csr
关键:
O: system:masters是 K8s 内置的超级管理员组,拥有 cluster-admin 权限。
6.1.7 创建 kube-controller-manager 证书¶
cat > kube-controller-manager-csr.json <<EOF
{
"CN": "system:kube-controller-manager",
"hosts": [],
"key": { "algo": "rsa", "size": 2048 },
"names": [
{ "C": "CN", "ST": "ChongQing", "L": "ChongQing", "O": "system:kube-controller-manager", "OU": "System" }
]
}
EOF
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kube-controller-manager-csr.json | cfssljson -bare kube-controller-manager
# 2026/08/04 23:08:43 [INFO] generate received request
# 2026/08/04 23:08:43 [INFO] received CSR
# 2026/08/04 23:08:43 [INFO] generating key: rsa-2048
# 2026/08/04 23:08:43 [INFO] encoded CSR
# 2026/08/04 23:08:43 [INFO] signed certificate with serial number 703132467317140343274167949161179421868048755625
# 2026/08/04 23:08:43 [WARNING] This certificate lacks a "hosts" field. This makes it unsuitable for
# websites. For more information see the Baseline Requirements for the Issuance and Management
# of Publicly-Trusted Certificates, v.1.1.6, from the CA/Browser Forum (https://cabforum.org);
# specifically, section 10.2.3 ("Information Requirements").
root@k8s-master-1:/k8s/kubernetes/ssl# ll -trh kube-controller-manager*
# -rw-r--r-- 1 root root 242 Aug 4 23:08 kube-controller-manager-csr.json
# -rw-r--r-- 1 root root 1.5K Aug 4 23:08 kube-controller-manager.pem
# -rw------- 1 root root 1.7K Aug 4 23:08 kube-controller-manager-key.pem
# -rw-r--r-- 1 root root 1.1K Aug 4 23:08 kube-controller-manager.csr
6.1.8 创建 kube-scheduler 证书¶
cat > kube-scheduler-csr.json <<EOF
{
"CN": "system:kube-scheduler",
"hosts": [],
"key": { "algo": "rsa", "size": 2048 },
"names": [
{ "C": "CN", "ST": "ChongQing", "L": "ChongQing", "O": "system:kube-scheduler", "OU": "System" }
]
}
EOF
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kube-scheduler-csr.json | cfssljson -bare kube-scheduler
# 2026/08/04 23:09:24 [INFO] generate received request
# 2026/08/04 23:09:24 [INFO] received CSR
# 2026/08/04 23:09:24 [INFO] generating key: rsa-2048
# 2026/08/04 23:09:24 [INFO] encoded CSR
# 2026/08/04 23:09:24 [INFO] signed certificate with serial number 526555129297671688636084518781266145933182612243
# 2026/08/04 23:09:24 [WARNING] This certificate lacks a "hosts" field. This makes it unsuitable for
# websites. For more information see the Baseline Requirements for the Issuance and Management
# of Publicly-Trusted Certificates, v.1.1.6, from the CA/Browser Forum (https://cabforum.org);
# specifically, section 10.2.3 ("Information Requirements").
root@k8s-master-1:/k8s/kubernetes/ssl# ll -trh kube-scheduler*
# -rw-r--r-- 1 root root 224 Aug 4 23:09 kube-scheduler-csr.json
# -rw-r--r-- 1 root root 1.5K Aug 4 23:09 kube-scheduler.pem
# -rw------- 1 root root 1.7K Aug 4 23:09 kube-scheduler-key.pem
# -rw-r--r-- 1 root root 1.1K Aug 4 23:09 kube-scheduler.csr
6.1.9 创建 kube-proxy 证书¶
cat > kube-proxy-csr.json <<EOF
{
"CN": "system:kube-proxy",
"hosts": [],
"key": { "algo": "rsa", "size": 2048 },
"names": [
{ "C": "CN", "ST": "ChongQing", "L": "ChongQing", "O": "kubernetes", "OU": "System" }
]
}
EOF
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kube-proxy-csr.json | cfssljson -bare kube-proxy
# 2026/08/04 23:12:48 [INFO] generate received request
# 2026/08/04 23:12:48 [INFO] received CSR
# 2026/08/04 23:12:48 [INFO] generating key: rsa-2048
# 2026/08/04 23:12:48 [INFO] encoded CSR
# 2026/08/04 23:12:48 [INFO] signed certificate with serial number 675029354083990658327457068349371307765277959753
# 2026/08/04 23:12:48 [WARNING] This certificate lacks a "hosts" field. This makes it unsuitable for
# websites. For more information see the Baseline Requirements for the Issuance and Management
# of Publicly-Trusted Certificates, v.1.1.6, from the CA/Browser Forum (https://cabforum.org);
# specifically, section 10.2.3 ("Information Requirements").
root@k8s-master-1:/k8s/kubernetes/ssl# ll -trh kube-proxy*
# -rw-r--r-- 1 root root 209 Aug 4 23:12 kube-proxy-csr.json
# -rw-r--r-- 1 root root 1.4K Aug 4 23:12 kube-proxy.pem
# -rw------- 1 root root 1.7K Aug 4 23:12 kube-proxy-key.pem
# -rw-r--r-- 1 root root 1.1K Aug 4 23:12 kube-proxy.csr
6.1.10 创建 kubelet 证书(每节点独立生成)¶
⚠️ 测试环境也不建议共用证书:Node 授权模式(
--authorization-mode=RBAC,Node)要求 kubelet 证书 CN(system:node:<名称>)与注册的节点名必须一致——node-2 用 node-1 的证书注册会被拒绝(403 Unauthorized)。所以每个节点生成一份,hosts 只写该节点自己的 IP。
① 生成 k8s-node-1 的证书
cat > kubelet-node1-csr.json <<EOF
{
"CN": "system:node:k8s-node-1",
"hosts": ["172.20.65.70"],
"key": { "algo": "rsa", "size": 2048 },
"names": [
{ "C": "CN", "ST": "ChongQing", "L": "ChongQing", "O": "system:nodes", "OU": "System" }
]
}
EOF
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kubelet-node1-csr.json | cfssljson -bare kubelet-node1
# 2026/08/04 23:19:01 [INFO] generate received request
# 2026/08/04 23:19:01 [INFO] received CSR
# 2026/08/04 23:19:01 [INFO] generating key: rsa-2048
# 2026/08/04 23:19:01 [INFO] encoded CSR
# 2026/08/04 23:19:01 [INFO] signed certificate with serial number 443612993290548524780976049449280349209719379989
root@k8s-master-1:/k8s/kubernetes/ssl# ll -trh kubelet-node1*
# -rw-r--r-- 1 root root 230 Aug 5 00:39 kubelet-node1-csr.json
# -rw-r--r-- 1 root root 1.5K Aug 5 00:39 kubelet-node1.pem
# -rw------- 1 root root 1.7K Aug 5 00:39 kubelet-node1-key.pem
# -rw-r--r-- 1 root root 1.1K Aug 5 00:39 kubelet-node1.csr
② 生成 k8s-node-2 的证书
cat > kubelet-node2-csr.json <<EOF
{
"CN": "system:node:k8s-node-2",
"hosts": ["172.20.65.71"],
"key": { "algo": "rsa", "size": 2048 },
"names": [
{ "C": "CN", "ST": "ChongQing", "L": "ChongQing", "O": "system:nodes", "OU": "System" }
]
}
EOF
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kubelet-node2-csr.json | cfssljson -bare kubelet-node2
# 2026/08/05 00:40:01 [INFO] generate received request
# 2026/08/05 00:40:01 [INFO] received CSR
# 2026/08/05 00:40:02 [INFO] generating key: rsa-2048
# 2026/08/05 00:40:02 [INFO] encoded CSR
# 2026/08/05 00:40:02 [INFO] signed certificate with serial number 655016981253341634770096563696998306956443537419
root@k8s-master-1:/k8s/kubernetes/ssl# ll -trh kubelet-node2*
# -rw-r--r-- 1 root root 230 Aug 5 00:39 kubelet-node2-csr.json
# -rw-r--r-- 1 root root 1.5K Aug 5 00:40 kubelet-node2.pem
# -rw------- 1 root root 1.7K Aug 5 00:40 kubelet-node2-key.pem
# -rw-r--r-- 1 root root 1.1K Aug 5 00:40 kubelet-node2.csr
关键:
O: system:nodes是 K8s 内置的 Node 授权组。CN格式为system:node:<hostname>,必须与该节点 kubelet 注册的节点名一致(7.3 的--hostname-override),hosts 写该节点自己的 IP。
证书汇总:
| 证书 | 用途 |
|---|---|
| ca.pem / ca-key.pem | 集群根证书 |
| kubernetes.pem / kubernetes-key.pem | API Server |
| admin.pem / admin-key.pem | kubectl 管理 |
| kube-controller-manager.pem / ...-key.pem | Controller Manager |
| kube-scheduler.pem / ...-key.pem | Scheduler |
| kube-proxy.pem / kube-proxy-key.pem | kube-proxy |
| kubelet-node1.pem / ...-key.pem(node-2 用 kubelet-node2.pem) | kubelet(每节点一份) |
6.2 部署 kube-apiserver¶
6.2.1 下载二进制包¶
从 Kubernetes Release 下载最新版本,以 v1.36.3 为例。
方式 A:Mac 下载后上传(服务器无外网 / 下载慢时推荐)
# ① Mac 本机下载并解压(Mac 自带 curl;-L 跟随重定向,-O 保存原文件名)
cd ~/Downloads
curl -LO https://dl.k8s.io/v1.36.3/kubernetes-server-linux-amd64.tar.gz
tar -zxf kubernetes-server-linux-amd64.tar.gz
# ② 上传整个 kubernetes 目录到 master-1
# ⚠️ 解压出的二进制是 linux/amd64 格式,在 Mac 上不能运行(会报 Exec format error),仅做传输
scp -r kubernetes root@172.20.65.69:/usr/local/src/
# ③ 以下在 k8s-master-1 节点执行:拷贝组件到目标位置
cd /usr/local/src
cp -p kubernetes/server/bin/{kube-apiserver,kube-controller-manager,kube-scheduler,kubectl} /k8s/kubernetes/bin/
cp -p kubernetes/server/bin/kubectl /usr/local/bin/
# 7.x 节需要:node 节点会从 master-1 分发这三个文件
cp -p kubernetes/server/bin/{kubelet,kube-proxy} /k8s/kubernetes/bin/ # crictl 不在 K8s 发布包内,单独安装(见 7.1)
方式 B:服务器直接下载(服务器可直连外网时)
cd /usr/local/src
wget https://dl.k8s.io/v1.36.3/kubernetes-server-linux-amd64.tar.gz
tar -zxf kubernetes-server-linux-amd64.tar.gz
cp -p kubernetes/server/bin/{kube-apiserver,kube-controller-manager,kube-scheduler,kubectl} /k8s/kubernetes/bin/
cp -p kubernetes/server/bin/kubectl /usr/local/bin/
cp -p kubernetes/server/bin/{kubelet,kube-proxy} /k8s/kubernetes/bin/ # crictl 不在 K8s 发布包内,单独安装(见 7.1)
6.2.2 创建 kube-apiserver 配置文件 kube-apiserver.conf¶
cat > /k8s/kubernetes/cfg/kube-apiserver.conf <<'EOF'
KUBE_APISERVER_OPTS="--etcd-servers=https://172.20.65.69:2379,https://172.20.65.70:2379,https://172.20.65.71:2379 \
--bind-address=172.20.65.69 \
--secure-port=6443 \
--advertise-address=172.20.65.69 \
--allow-privileged=true \
--service-cluster-ip-range=10.0.0.0/24 \
--service-node-port-range=30000-32767 \
--enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,ResourceQuota,NodeRestriction,MutatingAdmissionWebhook,ValidatingAdmissionWebhook \
--authorization-mode=RBAC,Node \
--enable-bootstrap-token-auth=true \
--kubelet-client-certificate=/k8s/kubernetes/ssl/kubernetes.pem \
--kubelet-client-key=/k8s/kubernetes/ssl/kubernetes-key.pem \
--tls-cert-file=/k8s/kubernetes/ssl/kubernetes.pem \
--tls-private-key-file=/k8s/kubernetes/ssl/kubernetes-key.pem \
--client-ca-file=/k8s/kubernetes/ssl/ca.pem \
--service-account-key-file=/k8s/kubernetes/ssl/ca-key.pem \
--service-account-signing-key-file=/k8s/kubernetes/ssl/ca-key.pem \
--service-account-issuer=https://kubernetes.default.svc.cluster.local \
--etcd-cafile=/k8s/etcd/ssl/ca.pem \
--etcd-certfile=/k8s/etcd/ssl/etcd.pem \
--etcd-keyfile=/k8s/etcd/ssl/etcd-key.pem \
--v=2 \
--audit-log-maxage=30 \
--audit-log-maxbackup=3 \
--audit-log-maxsize=100 \
--audit-log-path=/k8s/kubernetes/logs/k8s-audit.log"
EOF
v1.36 变化: -
--token-auth-file已在 v1.29 移除,改用 ServiceAccount 和 Bootstrap Token 认证 - 新增--service-account-issuer(必须)、--service-account-signing-key-file(必须) - 准入控制器新增MutatingAdmissionWebhook、ValidatingAdmissionWebhook
6.2.3 创建 Admin kubeconfig(用于 kubectl)¶
cat > /k8s/kubernetes/cfg/admin.kubeconfig <<EOF
apiVersion: v1
clusters:
- cluster:
certificate-authority: /k8s/kubernetes/ssl/ca.pem
server: https://172.20.65.69:6443
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: admin
name: default
current-context: default
kind: Config
preferences: {}
users:
- name: admin
user:
client-certificate: /k8s/kubernetes/ssl/admin.pem
client-key: /k8s/kubernetes/ssl/admin-key.pem
EOF
方便起见,将 kubeconfig 设为默认路径,以便 kubectl 自动读取:
6.2.4 创建 kube-apiserver 服务¶
cat > /usr/lib/systemd/system/kube-apiserver.service <<'EOF'
[Unit]
Description=Kubernetes API Server
Documentation=https://github.com/kubernetes/kubernetes
After=network.target
[Service]
EnvironmentFile=-/k8s/kubernetes/cfg/kube-apiserver.conf
ExecStart=/k8s/kubernetes/bin/kube-apiserver $KUBE_APISERVER_OPTS
Restart=on-failure
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
6.2.5 启动¶
查看状态和日志:
systemctl status kube-apiserver.service
journalctl -u kube-apiserver -f # v1.36 日志默认输出到 stderr(journald),不再写文件
6.3 部署 kube-controller-manager¶
重大变更(v1.20+):不能再使用
--master=127.0.0.1:8080连接 apiserver(insecure port 已移除),必须通过 kubeconfig 使用 TLS 证书认证。⚠️ kubeconfig 的
server必须写 apiserver 实际监听地址https://172.20.65.69:6443,不能写127.0.0.1——apiserver 的--bind-address=172.20.65.69只监听节点 IP,不回环地址。写 127.0.0.1 会导致组件dial tcp 127.0.0.1:6443: connect: connection refused无限重试(进程活着、kubectl get cs显示 Healthy 是假象,但所有控制器都不工作,DaemonSet/Deployment 等资源不会生成 Pod)。6.4 的 scheduler 同理。⚠️ v1.36 变化:
system:kube-controller-manager和system:kube-scheduler的内置 ClusterRole 不再是全权限(新版按最小权限授权,只有 events/leases/secrets 等)。二进制部署用证书身份直连,需手动补全权限绑定,否则 controller-manager 报configmaps is forbidden/nodes is forbidden:
6.3.1 创建 kube-controller-manager 的 kubeconfig¶
cat > /k8s/kubernetes/cfg/kube-controller-manager.kubeconfig <<EOF
apiVersion: v1
clusters:
- cluster:
certificate-authority: /k8s/kubernetes/ssl/ca.pem
server: https://172.20.65.69:6443
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: system:kube-controller-manager
name: default
current-context: default
kind: Config
preferences: {}
users:
- name: system:kube-controller-manager
user:
client-certificate: /k8s/kubernetes/ssl/kube-controller-manager.pem
client-key: /k8s/kubernetes/ssl/kube-controller-manager-key.pem
EOF
6.3.2 创建配置文件 kube-controller-manager.conf¶
cat > /k8s/kubernetes/cfg/kube-controller-manager.conf <<'EOF'
KUBE_CONTROLLER_MANAGER_OPTS="--leader-elect=true \
--kubeconfig=/k8s/kubernetes/cfg/kube-controller-manager.kubeconfig \
--bind-address=127.0.0.1 \
--allocate-node-cidrs=true \
--cluster-cidr=10.244.0.0/16 \
--service-cluster-ip-range=10.0.0.0/24 \
--cluster-signing-cert-file=/k8s/kubernetes/ssl/ca.pem \
--cluster-signing-key-file=/k8s/kubernetes/ssl/ca-key.pem \
--root-ca-file=/k8s/kubernetes/ssl/ca.pem \
--service-account-private-key-file=/k8s/kubernetes/ssl/ca-key.pem \
--cluster-signing-duration=87600h0m0s \
--v=2"
EOF
关键变化:
--master=127.0.0.1:8080替换为--kubeconfig=...。此外--address改为--bind-address。
6.3.3 创建服务并启动¶
cat > /usr/lib/systemd/system/kube-controller-manager.service <<'EOF'
[Unit]
Description=Kubernetes Controller Manager
After=network.target
[Service]
EnvironmentFile=/k8s/kubernetes/cfg/kube-controller-manager.conf
ExecStart=/k8s/kubernetes/bin/kube-controller-manager $KUBE_CONTROLLER_MANAGER_OPTS
Restart=on-failure
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl start kube-controller-manager
systemctl enable kube-controller-manager
6.4 部署 kube-scheduler¶
6.4.1 创建 kube-scheduler 的 kubeconfig¶
cat > /k8s/kubernetes/cfg/kube-scheduler.kubeconfig <<EOF
apiVersion: v1
clusters:
- cluster:
certificate-authority: /k8s/kubernetes/ssl/ca.pem
server: https://172.20.65.69:6443
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: system:kube-scheduler
name: default
current-context: default
kind: Config
preferences: {}
users:
- name: system:kube-scheduler
user:
client-certificate: /k8s/kubernetes/ssl/kube-scheduler.pem
client-key: /k8s/kubernetes/ssl/kube-scheduler-key.pem
EOF
6.4.2 创建配置文件 kube-scheduler.conf¶
cat > /k8s/kubernetes/cfg/kube-scheduler.conf <<'EOF'
KUBE_SCHEDULER_OPTS="--leader-elect=true \
--kubeconfig=/k8s/kubernetes/cfg/kube-scheduler.kubeconfig \
--bind-address=127.0.0.1 \
--v=2"
EOF
6.4.3 创建服务并启动¶
cat > /usr/lib/systemd/system/kube-scheduler.service <<'EOF'
[Unit]
Description=Kubernetes Scheduler
After=network.target
[Service]
EnvironmentFile=/k8s/kubernetes/cfg/kube-scheduler.conf
ExecStart=/k8s/kubernetes/bin/kube-scheduler $KUBE_SCHEDULER_OPTS
Restart=on-failure
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl start kube-scheduler
systemctl enable kube-scheduler
6.5 查看集群状态¶
kubectl get cs
# Warning: v1 ComponentStatus is deprecated in v1.19+
# NAME STATUS MESSAGE ERROR
# scheduler Healthy ok
# etcd-0 Healthy ok
# controller-manager Healthy ok
注意:
kubectl get cs(ComponentStatus)在 v1.19+ 已默认禁用,可通过 apiserver 的--feature-gates=ComponentStatuses=true启用,但推荐使用kubectl get --raw /healthz和kubectl get --raw /readyz检查组件健康状态。⚠️ 注意:
get cs显示 Healthy 只代表进程活着(healthz 端口在监听),不代表组件能连上 apiserver——实测 controller-manager 连不上 apiserver(kubeconfig server 写错)时get cs依然显示 Healthy,直到 Flannel 需要它干活才暴露(见 6.3 的 ⚠️)。⚠️
kubectl logs/kubectl exec报Forbidden (user=kubernetes, ...):kubectl logs由 apiserver 代理到 kubelet 执行,代理时使用 apiserver 自己的 kubelet 客户端证书(CN=kubernetes,6.1 的 kubernetes.pem)。kubelet 是 Webhook 授权模式,转回 apiserver 校验该身份权限——需要手动绑定system:kubelet-api-admin(kubeadm 自动创建,二进制部署必须手动):
kubectl get --raw /healthz
# ok
kubectl get --raw='/healthz?verbose'
# [+]ping ok
# [+]log ok
# [+]loopback-serving-certificate ok
# [+]etcd ok
# [+]poststarthook/start-apiserver-admission-initializer ok
# [+]poststarthook/generic-apiserver-start-informers ok
# [+]poststarthook/priority-and-fairness-config-consumer ok
# [+]poststarthook/priority-and-fairness-filter ok
# [+]poststarthook/storage-object-count-tracker-hook ok
# [+]poststarthook/start-apiextensions-informers ok
# [+]poststarthook/start-apiextensions-controllers ok
# [+]poststarthook/crd-informer-synced ok
# [+]poststarthook/start-system-namespaces-controller ok
# [+]poststarthook/peer-endpoint-reconciler-controller ok
# [+]poststarthook/start-cluster-authentication-info-controller ok
# [+]poststarthook/start-kube-apiserver-identity-lease-controller ok
# [+]poststarthook/start-kube-apiserver-identity-lease-garbage-collector ok
# [+]poststarthook/storage-readiness ok
# [+]poststarthook/start-legacy-token-tracking-controller ok
# [+]poststarthook/start-service-ip-repair-controllers ok
# [+]poststarthook/rbac/bootstrap-roles ok
# [+]poststarthook/scheduling/bootstrap-system-priority-classes ok
# [+]poststarthook/priority-and-fairness-config-producer ok
# [+]poststarthook/priority-and-fairness-config-consumer ok
# [+]poststarthook/priority-and-fairness-filter ok
# [+]poststarthook/bootstrap-controller ok
# [+]poststarthook/start-kubernetes-service-cidr-controller ok
# [+]poststarthook/start-kube-aggregator-informers ok
# [+]poststarthook/apiservice-status-local-available-controller ok
# [+]poststarthook/apiservice-status-remote-available-controller ok
# [+]poststarthook/apiservice-registration-controller ok
# [+]poststarthook/apiservice-discovery-controller ok
# [+]poststarthook/kube-apiserver-autoregistration ok
# [+]autoregister-completion ok
# [+]poststarthook/apiservice-openapi-controller ok
# [+]poststarthook/apiservice-openapiv3-controller ok
# healthz check passed
kubectl get --raw='/readyz'
# ok
或者更直接地检查各组件端口:
curl -k https://172.20.65.69:6443/healthz
# ok
curl -k https://127.0.0.1:10257/healthz # controller-manager
# ok
curl -k https://127.0.0.1:10259/healthz # scheduler
# ok
七、部署 Node 组件¶
7.1 安装容器运行时(containerd)¶
重要变化:K8s v1.24 移除了 dockershim,Docker 不再作为内置运行时。当前主流使用 containerd 或 CRI-O。以下以 containerd 为例。
所有 Node 节点执行(172.20.65.70, 172.20.65.71):
# 安装 containerd
# CentOS / RHEL(Docker 官方 yum 源)
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum install -y containerd.io
# Debian / Ubuntu(二选一)
# 方式 1(推荐,简单):Debian 官方源自带 containerd(版本较新,足够使用)
apt update && apt install -y containerd
# 方式 2(与 CentOS 同一套软件):Docker 官方 apt 源装 containerd.io
apt update && apt install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| tee /etc/apt/sources.list.d/docker.list
apt update && apt install -y containerd.io
# 以下步骤 CentOS / Debian 相同:
# 生成默认配置
mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml
# 关键:启用 SystemdCgroup(与 kubelet 的 cgroup 驱动一致)
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
# ⚠️ Debian 打包的 containerd 默认 cni bin_dir 是 /usr/lib/cni,而 CNI 插件(7.6)在 /opt/cni/bin——
# 不修正则所有 Pod sandbox 网络创建失败:
# "failed to find plugin \"flannel\" in path [/usr/lib/cni]"
# 检查并改为 /opt/cni/bin(`containerd config default` 生成的标准值;若已是则无需改)
grep -A3 'io.containerd.grpc.v1.cri".cni' /etc/containerd/config.toml
sed -i 's|bin_dir = "/usr/lib/cni"|bin_dir = "/opt/cni/bin"|' /etc/containerd/config.toml
# 国内网络:配置 registry.k8s.io 镜像加速(pause 等基础镜像走加速器,否则 Pod sandbox 创建失败)
cat >> /etc/containerd/config.toml <<EOF
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.k8s.io"]
endpoint = ["https://docker.m.daocloud.io"]
EOF
# 启动
systemctl start containerd
systemctl enable containerd
验证(containerd 自带 ctr 命令,无需额外工具;crictl 要等 7.2 分发后才在 node 上可用):
crictl是 K8s 的标准容器运行时 CLI 工具,不在 K8s 发布包内(属于独立的 cri-tools 项目)。在 k8s-master-1 上安装(node 节点不用单独装,7.2 分发后自动获得):
7.2 分发 Node 证书和二进制¶
在 k8s-node-1 节点上创建工作目录:
从 k8s-master-1 拷贝文件:
# 二进制
scp -r root@k8s-master-1:/k8s/kubernetes/bin/{kubelet,kube-proxy,crictl} /k8s/kubernetes/bin/
# 证书(node-1 用 kubelet-node1.pem;node-2 用 6.1.10 生成的 kubelet-node2.pem,见 7.5)
scp -r root@k8s-master-1:/k8s/kubernetes/ssl/{ca.pem,kubelet-node1.pem,kubelet-node1-key.pem,kube-proxy.pem,kube-proxy-key.pem} /k8s/kubernetes/ssl/
分发完成后,node 节点上的
crictl才可用。注意:crictl 在/k8s/kubernetes/bin/(不在 PATH),先加 PATH 再验证:echo 'export PATH=$PATH:/k8s/kubernetes/bin' > /etc/profile.d/k8s.sh source /etc/profile.d/k8s.sh # 配置 endpoint(消除 "Config /etc/crictl.yaml does not exist" 警告,可选) cat > /etc/crictl.yaml <<EOF runtime-endpoint: unix:///run/containerd/containerd.sock image-endpoint: unix:///run/containerd/containerd.sock timeout: 10 debug: false EOF crictl version crictl images # 连接 containerd(需已启动)crictl version输出的Version: 0.1.0是 CRI 协议版本(正常),RuntimeVersion才是 containerd 版本。
7.3 安装 kubelet¶
7.3.1 创建 kubelet 的 kubeconfig¶
cat > /k8s/kubernetes/cfg/kubelet.kubeconfig <<EOF
apiVersion: v1
clusters:
- cluster:
certificate-authority: /k8s/kubernetes/ssl/ca.pem
server: https://172.20.65.69:6443
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: system:node:k8s-node-1
name: default
current-context: default
kind: Config
preferences: {}
users:
- name: system:node:k8s-node-1
user:
client-certificate: /k8s/kubernetes/ssl/kubelet-node1.pem
client-key: /k8s/kubernetes/ssl/kubelet-node1-key.pem
EOF
server为什么是172.20.65.69:6443? 这个地址 = apiserver 所在位置(k8s-master-1)。apiserver 用--bind-address=172.20.65.69只监听 master-1 的节点 IP,所以: - 不能写127.0.0.1:那是 kubelet 自己(本机回环),kubelet 在 node 节点上,跨机器访问必须用 apiserver 的真实 IP - 不是 LB(172.20.65.74):LB 还没部署(第九节),现在只有 master-1 一个 apiserver;9.3 节会统一把所有 kubeconfig 的 server 改成 VIP(高可用入口) - 该地址必须在kubernetes.pem证书的 SAN(hosts)中,否则 TLS 握手报certificate is valid for X, not Y(6.1 已包含)server 地址 = 客户端眼中「控制平面的门牌号」。现在门只有 master-1 一个,地址就是 172.20.65.69:6443;等 LB 部署后统一换成 VIP。
简化说明:原文使用 token.csv + TLS Bootstrap + CSR 审批流程来颁发 kubelet 证书。
--token-auth-file已在 K8s v1.29 移除。此处改为直接使用预生成的证书,更简单直接。生产环境如需证书自动轮换,可使用 Bootstrap Token API(kubeadm token create方式)。
7.3.2 创建 kubelet 配置文件 kubelet-config.yml¶
cat > /k8s/kubernetes/cfg/kubelet-config.yml <<'EOF'
kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
address: 0.0.0.0
port: 10250
cgroupDriver: systemd
podInfraContainerImage: registry.k8s.io/pause:3.8 # 与 7.6.2 镜像拉取的 pause 版本保持一致(3.8 为 containerd 默认,实测通过)
clusterDNS:
- 10.0.0.2
clusterDomain: cluster.local
failSwapOn: false
authentication:
anonymous:
enabled: false
webhook:
cacheTTL: 2m0s
enabled: true
x509:
clientCAFile: /k8s/kubernetes/ssl/ca.pem
authorization:
mode: Webhook
webhook:
cacheAuthorizedTTL: 5m0s
cacheUnauthorizedTTL: 30s
evictionHard:
imagefs.available: 15%
memory.available: 100Mi
nodefs.available: 10%
nodefs.inodesFree: 5%
maxOpenFiles: 100000
maxPods: 110
EOF
关键变化: -
cgroupDriver改为systemd(与 containerd 配置一致) -readOnlyPort: 10255已废弃并移除 -apiVersion: kubelet.config.k8s.io/v1beta1在 v1.36 中仍可用,但建议关注升级路径
7.3.3 创建 kubelet 服务配置文件 kubelet.conf¶
cat > /k8s/kubernetes/cfg/kubelet.conf <<'EOF'
KUBELET_OPTS="--hostname-override=k8s-node-1 \
--container-runtime-endpoint=unix:///var/run/containerd/containerd.sock \
--kubeconfig=/k8s/kubernetes/cfg/kubelet.kubeconfig \
--config=/k8s/kubernetes/cfg/kubelet-config.yml \
--v=2"
EOF
关键变化: - 新增
--container-runtime-endpoint=unix:///var/run/containerd/containerd.sock(替代 Docker socket) - ⚠️ v1.36 已移除--network-plugin=cni/--cni-bin-dir/--cni-conf-dir(v1.33 起废弃)。原因:CNI 插件现在由容器运行时(containerd)直接调用,不再经过 kubelet——containerd 的 config.toml 默认 cni 段(bin_dir=/opt/cni/bin、conf_dir=/etc/cni/net.d),7.6 节 Flannel 的插件和配置放到这两个路径后 containerd 自动读取,kubelet 不需要任何网络参数 - ⚠️ v1.36 已移除--pod-infra-container-image(pause 镜像),改由 kubelet-config.yml 的podInfraContainerImage字段配置(见 7.3.2) - 移除--cgroups-per-qos和--enforce-node-allocatable(不再需要 workaround) - pause 镜像地址改为registry.k8s.io/pause:3.8(与 7.6.2 镜像拉取一致) - 移除--bootstrap-kubeconfig和--cert-dir(不再使用 TLS Bootstrap)
7.3.4 创建 kubelet 服务并启动¶
cat > /usr/lib/systemd/system/kubelet.service <<'EOF'
[Unit]
Description=Kubernetes Kubelet
After=containerd.service
[Service]
EnvironmentFile=/k8s/kubernetes/cfg/kubelet.conf
ExecStart=/k8s/kubernetes/bin/kubelet $KUBELET_OPTS
Restart=on-failure
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl start kubelet
systemctl enable kubelet
systemctl status kubelet
# ● kubelet.service - Kubernetes Kubelet
# Loaded: loaded (/usr/lib/systemd/system/kubelet.service; enabled; preset: enabled)
# Active: active (running) since Wed 2026-08-05 09:34:36 CST; 14min ago
# Invocation: 5388674a3d354e58819cf17035badae8
# Main PID: 1532 (kubelet)
# Tasks: 105:245m (limit: 4236)
# Memory: 29.7M (peak: 30.2M)
# CPU: 7.139s
# CGroup: /system.slice/kubelet.service
# └─5:245m1532 /k8s/kubernetes/bin/kubelet --hostname-override=k8s-node-1 --container-runtime-endpoint=unix:///var/run/containerd/containerd.sock --kubeconfig=/k8s/kubernetes/cfg/kubelet.kubeconfig>
#
# Aug 05 09:48:42 k8s-node-1 kubelet[1532]: I0805 09:48:42.106764 1532 kubelet.go:3262] "Container runtime network not ready" networkReady="NetworkReady=false reason:NetworkPluginNotReady message:Netwo>
# Aug 05 09:48:47 k8s-node-1 kubelet[1532]: I0805 09:48:47.108485 1532 kubelet.go:3262] "Container runtime network not ready" networkReady="NetworkReady=false reason:NetworkPluginNotReady message:Netwo>
# Aug 05 09:48:52 k8s-node-1 kubelet[1532]: I0805 09:48:52.109938 1532 kubelet.go:3262] "Container runtime network not ready" networkReady="NetworkReady=false reason:NetworkPluginNotReady message:Netwo>
# Aug 05 09:48:57 k8s-node-1 kubelet[1532]: I0805 09:48:57.111214 1532 kubelet.go:3262] "Container runtime network not ready" networkReady="NetworkReady=false reason:NetworkPluginNotReady message:Netwo>
# Aug 05 09:49:02 k8s-node-1 kubelet[1532]: I0805 09:49:02.113210 1532 kubelet.go:3262] "Container runtime network not ready" networkReady="NetworkReady=false reason:NetworkPluginNotReady message:Netwo>
# Aug 05 09:49:07 k8s-node-1 kubelet[1532]: I0805 09:49:07.115178 1532 kubelet.go:3262] "Container runtime network not ready" networkReady="NetworkReady=false reason:NetworkPluginNotReady message:Netwo>
# Aug 05 09:49:12 k8s-node-1 kubelet[1532]: I0805 09:49:12.116727 1532 kubelet.go:3262] "Container runtime network not ready" networkReady="NetworkReady=false reason:NetworkPluginNotReady message:Netwo>
# Aug 05 09:49:17 k8s-node-1 kubelet[1532]: I0805 09:49:17.117879 1532 kubelet.go:3262] "Container runtime network not ready" networkReady="NetworkReady=false reason:NetworkPluginNotReady message:Netwo>
# Aug 05 09:49:22 k8s-node-1 kubelet[1532]: I0805 09:49:22.119466 1532 kubelet.go:3262] "Container runtime network not ready" networkReady="NetworkReady=false reason:NetworkPluginNotReady message:Netwo>
# Aug 05 09:49:27 k8s-node-1 kubelet[1532]: I0805 09:49:27.120463 1532 kubelet.go:3262] "Container runtime network not ready" networkReady="NetworkReady=false reason:NetworkPluginNotReady message:Netwo>
"Container runtime network not ready" NetworkPluginNotReady : CNI 网络插件(Flannel)还没装(7.6 节的事)。kubelet 每 5 秒检查一次网络就绪,装好 Flannel 后这个日志自动消失。
7.4 安装 kube-proxy¶
7.4.1 创建 kube-proxy 的 kubeconfig¶
cat > /k8s/kubernetes/cfg/kube-proxy.kubeconfig <<EOF
apiVersion: v1
clusters:
- cluster:
certificate-authority: /k8s/kubernetes/ssl/ca.pem
server: https://172.20.65.69:6443
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: kube-proxy
name: default
current-context: default
kind: Config
preferences: {}
users:
- name: kube-proxy
user:
client-certificate: /k8s/kubernetes/ssl/kube-proxy.pem
client-key: /k8s/kubernetes/ssl/kube-proxy-key.pem
EOF
7.4.2 创建 kube-proxy 配置 kube-proxy-config.yml¶
cat > /k8s/kubernetes/cfg/kube-proxy-config.yml <<'EOF'
kind: KubeProxyConfiguration
apiVersion: kubeproxy.config.k8s.io/v1alpha1
bindAddress: 0.0.0.0
metricsBindAddress: 0.0.0.0:10249
clientConnection:
kubeconfig: /k8s/kubernetes/cfg/kube-proxy.kubeconfig
hostnameOverride: k8s-node-1
clusterCIDR: 10.244.0.0/16 # Pod 网段,必须与 7.6 Flannel 的 Network 一致(用于 SNAT 判断)
mode: ipvs
ipvs:
scheduler: "rr"
iptables:
masqueradeAll: true
EOF
注意:
kubeproxy.config.k8s.io/v1alpha1在 v1.36 中仍然可用,但address字段改为bindAddress,metrisBindAddress改为metricsBindAddress。
7.4.3 安装 iptables + 加载内核模块(ipvs 模式必需,Debian 默认均缺失)¶
# Debian 默认没有 iptables/ipset 命令(最小安装不含;CentOS 自带可跳过)
# kube-proxy 即使 ipvs 模式也依赖 iptables 做 SNAT、ipset 管理规则集合,命令缺失直接启动失败
apt install -y iptables ipset
modprobe nf_conntrack
modprobe ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh
# 开机自动加载(重启后不丢;缺少会导致 kube-proxy 报 nf_conntrack_max 不存在)
cat > /etc/modules-load.d/k8s-ipvs.conf <<EOF
nf_conntrack
ip_vs
ip_vs_rr
ip_vs_wrr
ip_vs_sh
EOF
# 验证:/proc/sys/net/netfilter/nf_conntrack_max 存在即 OK
ls /proc/sys/net/netfilter/nf_conntrack_max
坑:kube-proxy(ipvs 模式)启动依赖三样 Debian 默认没有的东西,按报错顺序:①
iptables命令(报executable file not found in $PATH);②ipset命令(报error getting ipset version);③nf_conntrack内核模块(报open /proc/sys/net/netfilter/nf_conntrack_max: no such file or directory)。① ② 用apt install -y iptables ipset,③ 用modprobe+ modules-load.d 持久化。
7.4.4 创建服务并启动¶
cat > /k8s/kubernetes/cfg/kube-proxy.conf <<'EOF'
KUBE_PROXY_OPTS="--config=/k8s/kubernetes/cfg/kube-proxy-config.yml \
--v=2"
EOF
cat > /usr/lib/systemd/system/kube-proxy.service <<'EOF'
[Unit]
Description=Kubernetes Proxy
After=network.target
[Service]
EnvironmentFile=/k8s/kubernetes/cfg/kube-proxy.conf
ExecStart=/k8s/kubernetes/bin/kube-proxy $KUBE_PROXY_OPTS
Restart=on-failure
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl start kube-proxy
systemctl enable kube-proxy
# 查看报错
journalctl -u kube-proxy -n 20 --no-pager
7.5 部署其它 Node 节点¶
与 7.2-7.4 流程一致,关键差异:kubelet 证书用 node-2 自己的(6.1.10 生成的 kubelet-node2.pem):
# 在 k8s-node-2 上
mkdir -p /k8s/kubernetes/{bin,cfg,logs,ssl}
# 二进制(同 7.2)
scp -r root@k8s-master-1:/k8s/kubernetes/bin/{kubelet,kube-proxy,crictl} /k8s/kubernetes/bin/
# 证书:注意 kubelet 证书文件名不同
scp -r root@k8s-master-1:/k8s/kubernetes/ssl/{ca.pem,kubelet-node2.pem,kubelet-node2-key.pem,kube-proxy.pem,kube-proxy-key.pem} /k8s/kubernetes/ssl/
其余步骤把 k8s-node-1 替换为 k8s-node-2,注意四处:
kubelet.kubeconfig:user: system:node:k8s-node-2,client-certificate改为/k8s/kubernetes/ssl/kubelet-node2.pemKUBELET_OPTS:--hostname-override=k8s-node-2kube-proxy-config.yml:hostnameOverride: k8s-node-2(7.4.2 模板里写的是 k8s-node-1,容易漏)server统一https://172.20.65.69:6443(不变)
完成后查看节点(在 k8s-master-1 上执行——kubectl 只安装在 master-1,node 节点没有):
kubectl get node -o wide
# Node 处于 NotReady 状态是正常的,因为 CNI 网络插件尚未安装
# root@k8s-master-1:~# kubectl get node -o wide
# NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
# k8s-node-1 NotReady <none> 59m v1.36.3 172.20.65.70 <none> Debian GNU/Linux 13 (trixie) 6.12.96+deb13-amd64 (amd64) containerd://1.7.24
# k8s-node-2 NotReady <none> 4m23s v1.36.3 172.20.65.71 <none> Debian GNU/Linux 13 (trixie) 6.12.96+deb13-amd64 (amd64) containerd://1.7.24
7.6 部署 CNI 网络(Flannel)¶
7.6.1 下载 CNI 插件¶
在 所有 Node 节点 执行:
mkdir -p /opt/cni/bin /etc/cni/net.d
# 从 GitHub 下载(请检查最新版本;Debian 无 wget 时用 curl -LO)
wget https://github.com/containernetworking/plugins/releases/download/v1.6.0/cni-plugins-linux-amd64-v1.6.0.tgz
# curl -LO https://github.com/containernetworking/plugins/releases/download/v1.6.0/cni-plugins-linux-amd64-v1.6.0.tgz
tar zxf cni-plugins-linux-amd64-v1.6.0.tgz -C /opt/cni/bin/
7.6.2 部署 Flannel¶
在 k8s-master-1 执行:
# 下载 Flannel 配置
wget https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml
# 确认 Pod 网段(默认 10.244.0.0/16)
grep -A2 '"Network"' kube-flannel.yml
kubectl apply -f kube-flannel.yml
⚠️ 网段一致性:集群有两个网段,容易混淆——
| 网段 | 用途 | 配置位置 |
|---|---|---|
|
10.244.0.0/16| Pod 网段(Flannel 给 Pod 分配 IP) | kube-flannel.yml 的Network必须等于 kube-proxy-config.yml 的clusterCIDR(7.4.2 已统一为该值) ||
10.0.0.0/24| Service 网段 | apiserver--service-cluster-ip-range(7.2);10.0.0.1= kubernetes Service 的 ClusterIP、10.0.0.2= 7.7 CoreDNS |不一致的后果:kube-proxy 的 SNAT(masquerade)判断错误——Pod 访问 Service 时源地址伪装行为异常,跨节点 Pod 通信可能不通。改 Flannel 网段时务必同步改 kube-proxy-config.yml 并
systemctl restart kube-proxy。
镜像拉取(国内网络实战)
flannel 的镜像在 ghcr.io(v0.25+ 不再发布 Docker Hub,DaoCloud 代理返回 403),pause/CoreDNS 在 registry.k8s.io(直连超时)。国内可用源:
| 镜像 | 可用源 | 备注 |
|---|---|---|
| pause / coredns | registry.aliyuncs.com/google_containers/<镜像>:<版本> |
阿里云镜像站,国内直连 |
| flannel / flannel-cni-plugin | ghcr.nju.edu.cn/flannel-io/<镜像>:<版本> |
南京大学 ghcr 镜像站 |
拉取后需要改名成 yaml 引用的名字——⚠️ crictl 没有 tag 命令(CRI 协议不支持),用 containerd 原生客户端 ctr,且 CRI 镜像位于 k8s.io namespace:
# 所有 node 节点执行
crictl pull registry.aliyuncs.com/google_containers/pause:3.8
ctr -n k8s.io images tag registry.aliyuncs.com/google_containers/pause:3.8 registry.k8s.io/pause:3.8
crictl pull ghcr.nju.edu.cn/flannel-io/flannel:v0.28.8
ctr -n k8s.io images tag ghcr.nju.edu.cn/flannel-io/flannel:v0.28.8 docker.io/flannel/flannel:v0.28.8
crictl images # 确认(CRI 视角 = kubelet 视角,与 ctr -n k8s.io images list 一致)
三个要点: 1. imagePullPolicy:yaml 未显式设置时,非 latest 标签默认
IfNotPresent——本地有镜像就直接用,不触发拉取 2. ImagePullBackOff 退避:Pod 拉取失败后进入退避(最长 5 分钟),镜像就位后不会自动立即重试——kubectl delete pods -n kube-flannel --all强制重建即可 3. Flannel 崩溃排查:若 Pod 报Error,看日志kubectl logs -n kube-flannel <pod>——常见为Failed to check br_netfilter(4.2.6 的模块没加载/没持久化)
7.6.3 验证¶
# 新版 kube-flannel.yml(v0.25+)使用独立的 kube-flannel namespace(旧版在 kube-system)
kubectl get pods -n kube-flannel -o wide
# 每节点一个 flannel Pod,Running 即成功
kubectl get nodes -o wide
# 所有 Node 应该变为 Ready 状态
7.6.4 测试创建 Pod¶
kubectl create deployment web --image=nginx
kubectl expose deployment web --port=80 --type=NodePort
kubectl get svc web
# 通过 NodePort 访问 Nginx 验证网络正常
7.7 部署 CoreDNS¶
⚠️ 执行节点:第 ① ③ 步在 k8s-master-1,第 ② 步在 所有 node 节点(CoreDNS Pod 调度到 node 上跑,镜像须装在 node 的 containerd 里)。
① k8s-master-1:下载配置、替换占位符、确认镜像版本
# ⚠️ 该文件是 sed 模板(文件名 .sed),含 5 个占位符,必须先替换再 apply,
# 否则报 "The Service kube-dns is invalid: spec.clusterIPs[0]: Invalid value: CLUSTER_DNS_IP"
# Debian 无 wget 时用 curl -LO(curl 需再加 -o 改名:curl -L -o coredns.yaml <URL>)
wget -O coredns.yaml https://raw.githubusercontent.com/coredns/deployment/master/kubernetes/coredns.yaml.sed
# 替换占位符(CLUSTER_DNS_IP 必须 = 10.0.0.2,与 kubelet-config.yml 的 clusterDNS 一致)
sed -i 's/CLUSTER_DOMAIN/cluster.local/g' coredns.yaml
sed -i 's/CLUSTER_DNS_IP/10.0.0.2/g' coredns.yaml
sed -i 's/REVERSE_CIDRS/in-addr.arpa ip6.arpa/g' coredns.yaml
sed -i 's/UPSTREAMNAMESERVER/\/etc\/resolv.conf/g' coredns.yaml # 注意:模板里无下划线、无末尾 S
sed -i 's/STUBDOMAINS//g' coredns.yaml # 不需要 stub domain 时替换为空
grep -nE "[A-Z]{4,}" coredns.yaml | grep -vE "DNS|UDP|TCP|HTTP|TLS"
# 应无输出;若剩 NET_BIND_SERVICE 属正常(Linux capability 名,非占位符)
grep image coredns.yaml
# 输出 image: coredns/coredns:1.9.4 → 记下镜像名(Docker Hub,node 拉取走 docker.io 加速)
② 所有 node 节点:拉镜像(版本号用第①步查到的;yaml 直接用 docker.io 名,无需 ctr tag)
③ k8s-master-1:部署(确认 clusterIP 与 kubelet-config.yml 中 clusterDNS 一致 = 10.0.0.2)
验证 DNS(⚠️ 必须查完整 FQDN:busybox 的 nslookup 不支持搜索域拼接,查短名 kubernetes.default 会返回 NXDOMAIN——这是工具限制,不是集群问题):
kubectl run -it --rm --image=busybox:1.36 --restart=Never dns-test -- nslookup kubernetes.default.svc.cluster.local
# 解析出 10.0.0.1 即正常
#
# Server: 10.0.0.2
# Address: 10.0.0.2:53
#
#
# Name: kubernetes.default.svc.cluster.local
# Address: 10.0.0.1
#
# All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.
# If you don't see a command prompt, try pressing enter.
# warning: couldn't attach to pod/dns-test, falling back to streaming logs: unable to upgrade connection: container dns-test not found in pod dns-test_default
# Server: 10.0.0.2
# Address: 10.0.0.2:53
#
#
# Name: kubernetes.default.svc.cluster.local
# Address: 10.0.0.1
#
# pod "dns-test" deleted from default namespace
八、部署 Dashboard¶
Kubernetes Dashboard 提供了 Web 版管理界面,支持查看集群状态、创建/修改资源、查看日志、执行命令等。
8.1 部署 Dashboard¶
⚠️ 执行节点:
kubectl命令在 k8s-master-1;镜像拉取在 node 节点(Dashboard 是 Deployment,Pod 由 scheduler 调度,不固定节点)。
# ① 下载稳定版配置(v2.7.0 为最新稳定版;v3.x 仍是 alpha,勿用)
# Debian 无 wget 时用 curl -LO
wget -O dashboard.yaml https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml
# ② 部署
kubectl apply -f dashboard.yaml
# ③ 查看 Pod 调度到哪台 node(dashboard + metrics-scraper 各 1 个)
kubectl get pods -n kubernetes-dashboard -o wide
# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# dashboard-metrics-scraper-6c78786c6f-4dk7p 1/1 Running 0 18m 10.244.38.15 k8s-node-2 <none> <none>
# kubernetes-dashboard-9b55494f9-bkxtr 1/1 Running 0 18m 10.244.38.14 k8s-node-2 <none> <none>
镜像拉取(若 Pod 报 ImagePullBackOff):dashboard 镜像在 docker.io(
kubernetesui/dashboard:v2.7.0、kubernetesui/metrics-scraper:v1.0.9),在 Pod 调度到的 node 上执行:
# ④ 改为 NodePort 访问(默认是 ClusterIP)
kubectl patch svc -n kubernetes-dashboard kubernetes-dashboard -p '{"spec":{"type":"NodePort"}}'
# service/kubernetes-dashboard patched
kubectl get svc -n kubernetes-dashboard kubernetes-dashboard
# PORT(S) 里 443:<NodePort>/TCP → 浏览器打开 https://<任一node-IP>:<NodePort>
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# kubernetes-dashboard NodePort 10.0.0.111 <none> 443:31865/TCP 5m12s
浏览器 → https://<node-1公网IP>:<NodePort>
https://<node-2公网IP>:<NodePort>
需要开放对应 node 的端口。
8.2 登录授权¶
浏览器打开
https://<node-IP>:<NodePort>→ 选择 Token 方式 → 粘贴下方命令的输出。注意:Dashboard 使用自签证书,浏览器会提示不安全,点「高级 → 继续前往」即可(测试环境)。
Dashboard 支持 Kubeconfig 和 Token 两种认证方式,以下使用 Token 方式:
cat > kubernetes-adminuser.yaml <<'EOF'
apiVersion: v1
kind: ServiceAccount
metadata:
name: admin-user
namespace: kubernetes-dashboard
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: admin-user
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: admin-user
namespace: kubernetes-dashboard
EOF
kubectl apply -f kubernetes-adminuser.yaml
获取登录 Token:

九、多 Master 部署¶
9.1 部署 Master2 节点¶
① 在 k8s-master-2(172.20.65.73) 创建目录并拷贝文件
mkdir -p /k8s/kubernetes /k8s/etcd
# 拷贝 K8s 配置、证书、二进制
scp -r root@k8s-master-1:/k8s/kubernetes/{cfg,ssl,bin} /k8s/kubernetes/
cp /k8s/kubernetes/bin/kubectl /usr/local/bin/
# ⚠️ etcd 证书必须拷贝:master-2 的 apiserver 是 etcd 的客户端(--etcd-cafile 等指向 /k8s/etcd/ssl/),
# 即使 etcd 不部署在 master-2 上,apiserver 连接远端 etcd 也需要这些证书
scp -r root@k8s-master-1:/k8s/etcd/ssl /k8s/etcd/
# 拷贝 systemd 服务文件
scp root@k8s-master-1:/usr/lib/systemd/system/kube-* /usr/lib/systemd/system/
# ⚠️ 配置 kubectl 的 KUBECONFIG(否则 kubectl 会连默认的 localhost:8080 报 connection refused)
echo 'export KUBECONFIG=/k8s/kubernetes/cfg/admin.kubeconfig' >> ~/.bashrc
source ~/.bashrc
kubectl get nodes # 验证 kubectl 可用
② 修改 kube-apiserver.conf 中的 IP 为本机 IP
sed -i 's/--bind-address=172.20.65.69/--bind-address=172.20.65.73/' /k8s/kubernetes/cfg/kube-apiserver.conf
sed -i 's/--advertise-address=172.20.65.69/--advertise-address=172.20.65.73/' /k8s/kubernetes/cfg/kube-apiserver.conf
③ 启动组件
systemctl daemon-reload
systemctl start kube-apiserver kube-controller-manager kube-scheduler
systemctl enable kube-apiserver kube-controller-manager kube-scheduler
systemctl status kube-apiserver kube-controller-manager kube-scheduler
9.2 部署 Nginx 四层负载均衡¶
方案说明:阿里云 VPC 不支持 keepalived 虚拟 IP(VRRP 广播被隔离,VIP 无法浮动)。本环境采用双 nginx + 手动切换:平时 kubeconfig 指向 k8s-lb-master(172.20.65.74);该机故障时把所有 kubeconfig 的 server 改指向 k8s-lb-backup(172.20.65.72)并重启组件(见 9.3)。 如需自动化高可用,生产环境建议用阿里云 SLB(内网 TCP 6443,后端挂 master-1/master-2)替代本节。
9.2.1 双 nginx + 手动切换¶
在 k8s-lb-master 和 k8s-lb-backup 两台机器上(Debian):
① 安装 Nginx + stream 模块
apt update && apt install -y nginx libnginx-mod-stream
# ⚠️ Debian 把 nginx 模块拆成独立包:stream 四层代理在 libnginx-mod-stream,
# 只装 nginx 会报 unknown directive "stream"
② 配置 TCP 四层负载均衡(两台配置相同)
# stream 块必须写在 http 块之外——追加到 nginx.conf 末尾
cat >> /etc/nginx/nginx.conf <<'EOF'
stream {
log_format main '$remote_addr $upstream_addr - [$time_local] $status $upstream_bytes_sent';
access_log /var/log/nginx/k8s-access.log main;
upstream k8s-apiserver {
server 172.20.65.69:6443;
server 172.20.65.73:6443;
}
server {
listen 6443;
proxy_pass k8s-apiserver;
}
}
EOF
nginx -t # 测试配置(语法错误会在这里暴露)
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful
systemctl start nginx
systemctl enable nginx
systemctl status nginx
⚠️ 不要用
cat > /etc/nginx/conf.d/xxx.conf写 stream 块——Debian 的 conf.d 是 http 块内的 include,stream 写在里面会报"stream" directive is not allowed here。必须追加到主配置末尾(http 块之外)。
9.2.2 非阿里云环境(物理机 / VMware / 其他云):keepalived VIP 自动高可用(备选)¶
在 VRRP 广播可达的环境,用 keepalived 让 VIP 在主备 LB 间自动漂移——kubeconfig 指向 VIP,LB 故障自动切换、无需人工干预(对应 9.3 的手动切换脚本)。
# 两台 LB:nginx 配置同 9.2.1 的 ②(stream 四层转发),再加 keepalived
apt install -y keepalived
# 查实际网卡名(VMware 常见 ens192,物理机 eth0——勿照抄 ens33)
ip addr | grep "^[0-9]"
# 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
# 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
Master(k8s-lb-master) 创建 /etc/keepalived/keepalived.conf:
cat > /etc/keepalived/keepalived.conf <<'EOF'
vrrp_instance VI_1 {
state MASTER
interface eth0 # ← 改成实际网卡名
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.31.100/24 # ← VIP,换成与节点网段不冲突的地址
}
track_script {
check_nginx
}
}
EOF
Backup(k8s-lb-backup):state BACKUP、priority 90,其余相同。
cat > /etc/keepalived/keepalived.conf <<'EOF'
vrrp_instance VI_1 {
state BACKUP
interface eth0 # ← 改成实际网卡名
virtual_router_id 51
priority 90
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.31.100/24 # ← VIP,换成与节点网段不冲突的地址
}
track_script {
check_nginx
}
}
EOF
# 健康检查脚本 + 启动(两台)
cat > /etc/keepalived/check_nginx.sh <<'EOF'
#!/bin/bash
count=$(ps -ef | grep nginx | egrep -cv "grep|$$")
if [ "$count" -eq 0 ]; then exit 1; else exit 0; fi
EOF
chmod +x /etc/keepalived/check_nginx.sh
systemctl start keepalived && systemctl enable keepalived
此方案下 9.3 的 kubeconfig 指向 VIP(
https://192.168.31.100:6443)而非固定 LB IP——主备切换对客户端透明。⚠️ VIP 必须加进 kubernetes.pem 的 SAN(客户端连接什么地址,证书就必须包含什么地址,否则 TLS 握手报
certificate is valid for X, not <VIP>)。在 k8s-master-1 上操作:
# ① 在 6.1 的 kubernetes-csr.json 的 hosts 中补 VIP,重新生成证书 cd /k8s/kubernetes/ssl sed -i '/"172.20.65.74",/a\ "192.168.31.100",' kubernetes-csr.json grep -A8 '"hosts"' kubernetes-csr.json # 确认 VIP 已加入 cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kubernetes-csr.json | cfssljson -bare kubernetes # ② 分发到两台 master 并重启 apiserver scp kubernetes.pem kubernetes-key.pem root@172.20.65.73:/k8s/kubernetes/ssl/ systemctl restart kube-apiserver # 两台 master 都要执行
9.2.3 阿里云生产环境:SLB 托管负载均衡(推荐)¶
阿里云 VPC 内手动切换不够优雅、keepalived 又不可用——生产环境用 SLB(负载均衡):完全托管、自身高可用、master 故障自动摘除,无需维护 LB 机器。
① 控制台创建 SLB(网络型 NLB 或传统型 CLB):
- 实例类型:VPC 内网型(按量付费,费用极低)
- 监听协议:TCP,端口 6443
- 后端服务器:k8s-master-1(172.20.65.69:6443)+ k8s-master-2(172.20.65.73:6443)
- 健康检查:TCP 探活即可(默认配置即可用)
- 记下 SLB 的内网 VIP(例如 172.20.65.75)
② kubeconfig 指向 SLB VIP:node 节点的 kubelet/kube-proxy 指向 SLB VIP(172.20.65.75);master-2 的 controller-manager/scheduler 不能指向 SLB VIP(NLB 后端访问 VIP 会环路,见 9.3.3 的 ⚠️),直连本机 172.20.65.73。具体 sed 命令见 9.3.3。
③ ⚠️ SLB VIP 必须加入 kubernetes.pem 的 SAN(6.1 生成证书时 hosts 补上 SLB IP,否则 TLS 握手失败)。
在 k8s-master-1 上操作:
# ① 在 6.1 的 kubernetes-csr.json 的 hosts 中补 VIP,重新生成证书
cd /k8s/kubernetes/ssl
sed -i '/"172.20.65.74",/a\ "172.20.65.75",' kubernetes-csr.json
grep -A8 '"hosts"' kubernetes-csr.json # 确认 VIP 已加入
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kubernetes-csr.json | cfssljson -bare kubernetes
# ② 分发到两台 master 并重启 apiserver
scp kubernetes.pem kubernetes-key.pem root@172.20.65.73:/k8s/kubernetes/ssl/
systemctl restart kube-apiserver # 两台 master 都要执行
④ 两台 LB 机器(172.20.65.74/72)可闲置或回收(nginx 方案不再需要)。
9.2.4 三种方案对比¶
| 方案 | 适用环境 | LB 故障时 | 运维成本 |
|---|---|---|---|
| 双 nginx + 手动切换 | 阿里云测试 | 手动 sed(~1 分钟) | 两台机器 |
| keepalived VIP | 物理机 / VMware / 其他云 | 自动漂移,无感知 | 两台机器 + 配置 |
| SLB | 阿里云生产 | 自动摘除,无感知 | 零(托管) |
9.3 各组件连接 LB 入口¶
所有访问 apiserver 的 kubeconfig 统一指向 LB 入口(入口地址按 9.2 方案选择:手动切换 = 172.20.65.74、keepalived = VIP、SLB = SLB 内网 VIP):
- node 节点:kubelet.kubeconfig、kube-proxy.kubeconfig
- master-2:kube-controller-manager.kubeconfig、kube-scheduler.kubeconfig(保证 master-1 故障时 master-2 的控制面组件仍能连上 apiserver)
- admin.kubeconfig 保持指向 master-1(运维直连通道,不依赖 LB)
以下三个方案的 sed 命令都以初始状态(server 指向 172.20.65.69)为源地址——若此前已切到其他入口(如 74),先把 sed 的源地址改成当前值再执行。
9.3.1 手动切换方案(入口 172.20.65.74,当前环境)¶
node-1、node-2 上执行(kubelet / kube-proxy):
sed -i 's|server: https://172.20.65.69:6443|server: https://172.20.65.74:6443|' /k8s/kubernetes/cfg/kubelet.kubeconfig
sed -i 's|server: https://172.20.65.69:6443|server: https://172.20.65.74:6443|' /k8s/kubernetes/cfg/kube-proxy.kubeconfig
systemctl restart kubelet kube-proxy
systemctl status kubelet kube-proxy
k8s-master-2 上执行(controller-manager / scheduler——master-2 没有 kubelet/kube-proxy 这两个文件,别跑错机器):
sed -i 's|server: https://172.20.65.69:6443|server: https://172.20.65.74:6443|' /k8s/kubernetes/cfg/kube-controller-manager.kubeconfig
sed -i 's|server: https://172.20.65.69:6443|server: https://172.20.65.74:6443|' /k8s/kubernetes/cfg/kube-scheduler.kubeconfig
systemctl restart kube-controller-manager kube-scheduler
systemctl status kube-controller-manager kube-scheduler
验证:
# 任意节点通过 LB 入口访问 apiserver
curl -k https://172.20.65.74:6443/healthz
# ok
# 真实客户端验证(kubeconfig 指向入口后)
kubectl get nodes
kubectl get cs
LB 故障手动切换(k8s-lb-master 宕机时):
# 把上面的 sed 命令中所有 172.20.65.74 换成 172.20.65.72(k8s-lb-backup),重新执行 + 重启组件
# 建议将切换写成一个脚本备用:
# sed -i 's|server: https://172.20.65.74:6443|server: https://172.20.65.72:6443|' /k8s/kubernetes/cfg/*.kubeconfig
# systemctl restart kubelet kube-proxy
# 恢复后反向执行即可
9.3.2 keepalived 方案(入口 192.168.31.100)¶
分工同 9.3.1:node-1/node-2 执行前两条,master-2 执行后两条。VIP 已按 9.2 的 ⚠️ 补进证书 SAN。
# node-1、node-2:
sed -i 's|server: https://172.20.65.69:6443|server: https://192.168.31.100:6443|' /k8s/kubernetes/cfg/kubelet.kubeconfig
sed -i 's|server: https://172.20.65.69:6443|server: https://192.168.31.100:6443|' /k8s/kubernetes/cfg/kube-proxy.kubeconfig
systemctl restart kubelet kube-proxy
# k8s-master-2:
sed -i 's|server: https://172.20.65.69:6443|server: https://192.168.31.100:6443|' /k8s/kubernetes/cfg/kube-controller-manager.kubeconfig
sed -i 's|server: https://172.20.65.69:6443|server: https://192.168.31.100:6443|' /k8s/kubernetes/cfg/kube-scheduler.kubeconfig
systemctl restart kube-controller-manager kube-scheduler
验证:
故障:停掉 k8s-lb-master 的 nginx 或 keepalived → VIP 自动漂移到 backup → 入口不变、无需任何操作(模拟故障后 curl 依然 ok 即验证通过)。
9.3.3 SLB 方案(入口 SLB 内网 VIP)¶
与 9.3.2 完全同理,仅入口 IP 换成 SLB 内网 VIP(示例 172.20.65.75,以控制台实际为准);SLB VIP 需已加入证书 SAN(9.2 的 SLB 小节 ⚠️)。
⚠️ NLB 特有坑:后端服务器不能访问 NLB VIP。NLB 的转发逻辑按哈希回落,后端服务器(master-1/master-2)访问 NLB VIP 时流量可能被转发回自己 → 环路 → SYN 丢失(curl 超时/几十秒重试)。因此 master-2 的 controller-manager/scheduler 的 kubeconfig 不能指向 SLB VIP,必须直连本机 apiserver(172.20.65.73);node 节点不是 SLB 后端,指向 VIP 没问题。手动切换方案(nginx)无此问题(nginx 四层代理不哈希回落)。
# node-1、node-2(node 不是 SLB 后端,指向 VIP 正常):
sed -i 's|server: https://172.20.65.69:6443|server: https://172.20.65.75:6443|' /k8s/kubernetes/cfg/kubelet.kubeconfig
sed -i 's|server: https://172.20.65.69:6443|server: https://172.20.65.75:6443|' /k8s/kubernetes/cfg/kube-proxy.kubeconfig
systemctl restart kubelet kube-proxy
# k8s-master-2(⚠️ 直连本机,不走 SLB VIP——后端访问 VIP 会环路):
sed -i 's|server: https://172.20.65.69:6443|server: https://172.20.65.73:6443|' /k8s/kubernetes/cfg/kube-controller-manager.kubeconfig
sed -i 's|server: https://172.20.65.69:6443|server: https://172.20.65.73:6443|' /k8s/kubernetes/cfg/kube-scheduler.kubeconfig
systemctl restart kube-controller-manager kube-scheduler
验证:
故障:停掉 master-1 的 apiserver → SLB 自动摘除后端 → 入口不变、无需任何操作。
三种 LB 方案的验证对照(验证逻辑相同,仅入口 IP 不同):
| 方案 | 入口 IP | 日常验证 | 故障演练(停一个后端) | 故障时入口 |
|---|---|---|---|---|
| 双 nginx + 手动切换 | 172.20.65.74 | curl -k https://172.20.65.74:6443/healthz + kubectl |
手动 sed 切到 72 → 恢复 | 变(74→72) |
| keepalived VIP | 192.168.31.100 | curl -k https://192.168.31.100:6443/healthz + kubectl |
停 nginx/keepalived → VIP 自动漂移 → 恢复(无需操作) | 不变 |
| SLB | SLB 内网 VIP | curl -k https://<SLB-VIP>:6443/healthz + kubectl |
停 master-1 apiserver → SLB 自动摘除 → 恢复(无需操作) | 不变 |
核心区别:验证方法相同,高可用的价值在故障时——手动方案要改入口(sed),自动方案(keepalived/SLB)入口不变、无需人工干预。
至此,高可用 K8s 集群部署完成。
扩展阅读¶
- Kubernetes kubeadm 生产部署实战 — 官方推荐的生产部署方式,对照本文的坑
- Kubernetes 核心架构 — 认识控制平面、工作节点、核心组件
- Kubernetes Pod 与 Deployment 实战 — 工作负载实战
- Vertica on Kubernetes 部署指南 — Vertica 在 K8s 上的快速入门
- Kubernetes 官方文档
- etcd 官方文档