跳转至

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 planupgrade 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 特性

  1. 自我修复:节点故障时自动重启失败的容器,保证预期的副本数量;健康检查失败的容器在未恢复前不接流量。
  2. 弹性伸缩:基于 CPU/内存等指标自动扩缩容,业务高峰期自动增加实例,低峰期回收资源。
  3. 自动部署和回滚:采用滚动更新策略逐步替换 Pod,出现问题自动回滚,确保升级不中断业务。
  4. 服务发现和负载均衡:为多个容器提供统一访问入口(ClusterIP + DNS 名称),自动负载均衡所有后端 Pod。
  5. 机密和配置管理:通过 Secret 和 ConfigMap 管理敏感数据和配置,不暴露在镜像中。
  6. 存储编排:支持挂载本地存储、公有云存储、网络存储(NFS/Ceph/GlusterFS 等)。
  7. 批处理:提供 Job 和 CronJob 资源,满足批量处理和定时任务场景。

二、集群架构与组件

Kubernetes 集群由 Master 节点(控制平面)和 Node 节点(工作负载)组成。

2.1 Master(控制平面)

Master 负责整个集群的管理和控制,生产环境建议部署多个 Master 保证高可用。核心组件:

  1. kube-apiserver:集群统一入口,所有资源操作的 REST API 网关,唯一与 etcd 直接交互的组件。
  2. kube-controller-manager:所有资源对象的自动化控制中心,每种资源对应一个控制器(Deployment Controller、Node Controller 等),遵循"观察 → 比较 → 执行"的调谐循环(Reconciliation Loop)。
  3. kube-scheduler:根据调度算法为新创建的 Pod 选择最优 Node 节点。
  4. etcd:分布式一致的 key-value 存储,保存集群所有状态数据,基于 Raft 共识算法。

2.2 Node(工作节点)

Node 是实际运行业务负载的节点,每个 Node 运行以下组件:

  1. kubelet:Master 在 Node 上的 Agent,管理本机 Pod 的生命周期,负责容器创建、启停、健康检查。
  2. kube-proxy:实现 Service 通信的网络代理,维护 iptables/IPVS 规则,提供四层负载均衡。
  3. 容器运行时:通过 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 取代,后者支持集合选择器(innotin),功能更强。

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 同机部署,只要网络可达。

单 Master 集群架构

4.1.2 多 Master 集群架构

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

多 Master 集群架构

4.1.3 集群规划

示例环境使用 6 台阿里云 ecs.e-c1m2.large 服务器,规划如下:

序号 角色 私网IP 主机名 组件
1 k8s-master-1 172.20.65.69 k8s-master-1
sudo hostnamectl set-hostname k8s-master-1
kube-apiserver
kube-controller-manager
kube-scheduler
etcd
2 k8s-master-2 172.20.65.73 k8s-master-2
sudo hostnamectl set-hostname k8s-master-2
kube-apiserver
kube-controller-manager
kube-scheduler
3 k8s-node-1 172.20.65.70 k8s-node-1
sudo hostnamectl set-hostname k8s-node-1
kubelet
kube-proxy
containerd
etcd
4 k8s-node-2 172.20.65.71 k8s-node-2
sudo hostnamectl set-hostname k8s-node-2
kubelet
kube-proxy
containerd
etcd
5 Load Balancer(Master) 172.20.65.74 k8s-lb-master
sudo hostnamectl set-hostname k8s-lb-master
Nginx L4
6 Load Balancer(Backup) 172.20.65.72 k8s-lb-backup
sudo 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 关闭防火墙

# CentOS / RHEL
systemctl stop firewalld
systemctl disable firewalld
# Debian / Ubuntu(默认没有 firewalld;如有 ufw 则关闭)
systemctl stop ufw 2>/dev/null
systemctl disable ufw 2>/dev/null

Debian 默认不装防火墙软件,阿里云安全组已做网络隔离,此步通常直接跳过即可。

4.2.2 关闭 SELinux

# CentOS / RHEL
setenforce 0
sed -i 's/enforcing/disabled/' /etc/selinux/config

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.cfgpreserve_hostname: true;主机名只能小写字母/数字/连字符)。

4.2.5 同步系统时间

# CentOS / RHEL
ntpdate time.windows.com  # 或 chronyd
# 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 statusendpoint health

5.4 Etcd 部署常见报错速查

以下为本部署流程实测踩过的坑(etcd 3.5.x + systemd 256 / Debian 13):

错误检查方法:

systemctl status etcd
journalctl -u etcd.service -n 30 --no-pager
报错特征 根因 修复
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

三条关键规则:

  1. systemd 的 ExecStart= 支持 ${VAR} 展开,但 WorkingDirectory= 不支持——后者必须写绝对路径
  2. etcd 3.5+ 禁止同一配置同时走 flag 和环境变量 → unit 文件用 EnvironmentFile 全量传参,ExecStart 写裸命令
  3. 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 保留。

mkdir -p /k8s/kubernetes/{ssl,cfg,bin,logs}
cd /k8s/kubernetes/ssl

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(必须) - 准入控制器新增 MutatingAdmissionWebhookValidatingAdmissionWebhook

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 自动读取:

mkdir -p ~/.kube
cp /k8s/kubernetes/cfg/admin.kubeconfig ~/.kube/config

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 daemon-reload
systemctl start kube-apiserver
systemctl enable kube-apiserver

查看状态和日志:

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-managersystem:kube-scheduler 的内置 ClusterRole 不再是全权限(新版按最小权限授权,只有 events/leases/secrets 等)。二进制部署用证书身份直连,需手动补全权限绑定,否则 controller-manager 报 configmaps is forbidden / nodes is forbidden

kubectl create clusterrolebinding ccm-fix --clusterrole=cluster-admin --user=system:kube-controller-manager
kubectl create clusterrolebinding scheduler-fix --clusterrole=cluster-admin --user=system:kube-scheduler

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 /healthzkubectl get --raw /readyz 检查组件健康状态。

⚠️ 注意get cs 显示 Healthy 只代表进程活着(healthz 端口在监听),不代表组件能连上 apiserver——实测 controller-manager 连不上 apiserver(kubeconfig server 写错)时 get cs 依然显示 Healthy,直到 Flannel 需要它干活才暴露(见 6.3 的 ⚠️)。

⚠️ kubectl logs / kubectl execForbidden (user=kubernetes, ...)kubectl logs 由 apiserver 代理到 kubelet 执行,代理时使用 apiserver 自己的 kubelet 客户端证书(CN=kubernetes,6.1 的 kubernetes.pem)。kubelet 是 Webhook 授权模式,转回 apiserver 校验该身份权限——需要手动绑定 system:kubelet-api-admin(kubeadm 自动创建,二进制部署必须手动):

kubectl create clusterrolebinding kubelet-api-admin-fix \
  --clusterrole=system:kubelet-api-admin \
  --user=kubernetes

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 上可用):

systemctl status containerd   # active (running)
ctr version                   # 输出 containerd 版本即安装成功

crictl 是 K8s 的标准容器运行时 CLI 工具,不在 K8s 发布包内(属于独立的 cri-tools 项目)。在 k8s-master-1 上安装(node 节点不用单独装,7.2 分发后自动获得):

# 以下在 k8s-master-1 节点执行
cd /usr/local/src
curl -LO https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.36.0/crictl-v1.36.0-linux-amd64.tar.gz
tar -zxf crictl-v1.36.0-linux-amd64.tar.gz
mv crictl /usr/local/bin/
cp /usr/local/bin/crictl /k8s/kubernetes/bin/   # 供 7.2 分发

7.2 分发 Node 证书和二进制

k8s-node-1 节点上创建工作目录:

mkdir -p /k8s/kubernetes/{bin,cfg,logs,ssl}

从 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/binconf_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 字段改为 bindAddressmetrisBindAddress 改为 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.kubeconfiguser: system:node:k8s-node-2client-certificate 改为 /k8s/kubernetes/ssl/kubelet-node2.pem
  • KUBELET_OPTS--hostname-override=k8s-node-2
  • kube-proxy-config.ymlhostnameOverride: 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

crictl pull coredns/coredns:1.9.4
crictl images | grep coredns   # 确认就位

③ k8s-master-1:部署(确认 clusterIP 与 kubelet-config.yml 中 clusterDNS 一致 = 10.0.0.2)

kubectl apply -f coredns.yaml

验证 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.0kubernetesui/metrics-scraper:v1.0.9),在 Pod 调度到的 node 上执行:

crictl pull kubernetesui/dashboard:v2.7.0
crictl pull kubernetesui/metrics-scraper:v1.0.9

# ④ 改为 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:

kubectl -n kubernetes-dashboard create token admin-user --duration=24h

Dashboard 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 BACKUPpriority 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 指向 VIPhttps://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

验证:

curl -k https://192.168.31.100:6443/healthz   # ok
kubectl get nodes

故障:停掉 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

验证:

curl -k https://172.20.65.75:6443/healthz   # ok
kubectl get nodes

故障:停掉 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 集群部署完成。

扩展阅读