基于二进制包方式搭建 Kubernetes 多 Master 高可用集群

大纲

高可用集群的介绍

Kubernetes 作为容器集群系统,通过健康检查与重启策略实现了 Pod 的故障自我修复能力,通过调度算法将 Pod 分布式部署,并持续监控其预期副本数。当节点发生故障时,系统能够自动在正常节点上重新启动 Pod,从而实现应用层的高可用性。在 Kubernetes 集群环境中,高可用性还需考虑以下两个层面:Etcd 存储库的高可用性,以及 Kubernetes Master 节点的高可用性。Etcd 可以部署 3 集群个节点来保证高可用性。Kubernetes Master 节点承担总控中心角色,通过持续与工作节点(Worker)上的 Kubelet 和 Kube-Proxy 进行通信,维护整个集群的健康工作状态。一旦 Master 节点发生故障,将无法使用 kubectl 工具或 API 进行集群管理。Kubernetes Master 节点主要包含三个核心服务:API Server、Controller Manager 和 Scheduler。其中,Controller Manager 和 Scheduler 组件自身已通过内置的 Leader 选举机制实现了高可用。因此,Kubernetes Master 节点的高可用主要是针对 API Server 组件,该组件以 HTTP API 形式提供服务,其高可用实现方式与 Web 服务器类似,可以通过负载均衡器(比如 HaProxy / Nginx)对其多个实例进行负载均衡,使其支持水平扩展,并可以使用 Keepalived 来保证负载均衡器自身的高可用性。

Kubernetes 高可用集群的两种搭建方式

Kubernetes 高可用集群通常有两种部署方式:二进制手动部署 和 Kubeadm 自动化部署。在生产环境中,更推荐使用二进制部署方式,因为可控性高、组件版本可精细管理,便于根据业务需求进行深度定制。而在开发或测试环境,则可以使用 Kubeadm 快速搭建高可用集群,部署流程简单、效率更高。 值得一提的是,如果之前是 基于二进制包方式搭建 Kubernetes 单 Master 集群 的,那么可以在此基础上对 Master 节点进行扩容,无需从零开始搭建 Kubernetes 多 Master 集群;此时,最关键的步骤是重新生成 Kubernetes 集群所需的证书,并部署 HaProxy 与 Keepalived,最后配置 Controller Manager、Scheduler、Kubelet、Kube-Proxy 这些组件使用 Keepalived 的 VIP 与 HaProxy 的反向代理端口去连接 API Server。

Kubernetes 多 Master 集群的整体架构如下,多个 Master 节点之间通过 HaProxy 实现反向代理和负载均衡,而 HaProxy 通过 Keepalived 来保证自身的高可用性(即双机主备,也叫双机热备)。

搭建多 Master 集群

本节将基于二进制包方式搭建多 Master 的 Kubernetes 集群,核心目标如下:

  • 为 Etcd 、Kubernetes 核心组件自签证书
  • 部署 Etcd 集群(3 个节点)
  • 部署 Kubernetes 集群的 2 个 Master 节点
  • 部署 Kubernetes 集群的 3 个 Node 节点
  • 部署 Kubernetes 集群的 CNI 网络插件(Flannel)
  • 部署 Kubernetes 集群的 DNS 组件(CoreDNS)
  • 部署 HaProxy、Keepalived,保证 API Server 的高可用性

术语说明

为了方便描述,本文使用 Node 节点 来替代 Kubernetes 的 Worker Node,使用 Master 节点 来替代 Master Node

版本与硬件

搭建 Kubernetes 高可用集群所使用的软件版本如下:

软件版本安装方式
CentOS7.9多个独立虚拟机
Docker19.03.9二进制安装包
Kubernetes1.19.10二进制安装包
Etcd3.4.9二进制安装包
HaProxy2.8.5源码编译安装
Keepalived2.2.8源码编译安装

搭建 Kubernetes 高可用集群所使用的硬件需要满足以下几个条件

  • 多台机器或虚拟机,建议操作系统 CentOS 7(64 位)
  • 集群中所有机器之间的网络可以互通
  • 系统内可以访问外网,需要拉取镜像
  • 系统禁用 Swap 分区(必须)
  • Master 节点的硬件配置要求
    • 当 Master 节点只运行运行控制平面组件(如 API Server、Controller Manager 和 Scheduler)
      • 2GB 或更多 RAM,2 个 CPU 或更多 CPU,硬盘 20GB 或更多
    • 当 Master 节点运行控制平面组件,且承载用户 Pod 的调度和运行任务
      • 6GB 或更多 RAM,6 个 CPU 或更多 CPU,硬盘 60GB 或更多
  • Node 节点的硬件配置要求
    • 4GB 或更多 RAM,4 个 CPU 或更多 CPU,硬盘 40GB 或更多

服务器规划

本文所使用的 Kubernetes 集群服务器规划如下表所示。为了节省资源,Etcd 集群部署在与 Kubernetes 集群相同的服务器上。需要注意的是,在生产环境中,强烈建议将 Etcd 集群部署在独立的服务器上,以提升系统的性能和高可用性。

Host 名称角色 IPCPU 核数内存磁盘安装的组件备注
k8s-master1master192.168.2.191>= 6C>=6G>=60Gkube-apiserver,kube-controller-manager,kube-scheduler,kubelet,kube-proxy,cni plugin, docker,etcd,haproxy,keepalived 部署 Etcd、HaProxy、Keepalived
k8s-node1node(worker)192.168.2.112>= 4C>=4G>=40Gkubelet,kube-proxy,cni plugin, docker,etcd 部署 Etcd
k8s-node2node(worker)192.168.2.131>= 4C>=4G>=40Gkubelet,kube-proxy,cni plugin, docker,etcd 部署 Etcd
k8s-node3node(worker)192.168.2.236>= 4C>=4G>=40Gkubelet,kube-proxy,cni plugin, docker 不部署 Etcd
k8s-master2master192.168.2.148>= 6C>=6G>=60Gkube-apiserver,kube-controller-manager,kube-scheduler,kubelet,kube-proxy,cni plugin, docker,haproxy,keepalived 不部署 Etcd,部署 HaProxy、Keepalived

如果希望 Master 节点仅用于运行控制平面组件(如 API Server、Controller Manager 和 Scheduler),不承载用户 Pod 的调度和运行任务,则 Master 节点可以不部署 Kubelet、Kube-Proxy 和 Docker(容器运行时),如下表所示:

Host 名称角色 IPCPU 核数内存磁盘安装的组件备注
k8s-master1master192.168.2.191>= 2C>=2G>=20Gkube-apiserver,kube-controller-manager,kube-scheduler,cni plugin,etcd,haproxy,keepalived 部署 Etcd、HaProxy、Keepalived,不部署 Kubelet、Kube-Proxy、Docker
k8s-node1node(worker)192.168.2.112>= 4C>=4G>=40Gkubelet,kube-proxy,cni plugin, docker,etcd 部署 Etcd
k8s-node2node(worker)192.168.2.131>= 4C>=4G>=40Gkubelet,kube-proxy,cni plugin, docker,etcd 部署 Etcd
k8s-node3node(worker)192.168.2.236>= 4C>=4G>=40Gkubelet,kube-proxy,cni plugin, docker 不部署 Etcd
k8s-master2master192.168.2.148>= 2C>=2G>=20Gkube-apiserver,kube-controller-manager,kube-scheduler,kubelet,kube-proxy,cni plugin, haproxy,keepalived 不部署 Etcd、Kubelet、Kube-Proxy、Docker,部署 HaProxy、Keepalived

虚拟 IP(VIP)的说明

  • 虚拟 IP(VIP)由 Keepalived 提供。
  • 在配置 Keepalived 的 virtual_ipaddress 参数时,所指定的 VIP 地址必须与 HaProxy 所在节点处于同一网段。
  • 例如,如果两个 HaProxy 节点的网段为 192.168.2.0/24,则 VIP 也应设置为该网段内的地址(如 192.168.2.x)。

操作系统初始化

特别注意

  • 在 Kubernetes 集群的所有节点(包括 Master 和 Node 节点)上,分别执行以下系统初始化操作

关闭防火墙

1
2
3
4
5
# 临时关闭
# systemctl stop firewalld

# 永久关闭
# systemctl disable firewalld

关闭 selinux

1
2
3
4
5
# 临时关闭
# setenforce 0

# 永久关闭
# sed -i 's/enforcing/disabled/' /etc/selinux/config

关闭 swap 分区

1
2
3
4
5
# 临时关闭
$ swapoff -a

# 永久关闭
# sed -i 's/.*swap.*/#&/' /etc/fstab

系统时间同步(强烈建议使用 chrony 而不是 ntpdate)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 卸载ntpdate服务
# yum remove -y ntpdate

# 安装chrony服务
# yum install -y chrony

# 开机自启动chrony服务
# systemctl enable chronyd

# 启动chrony服务
# systemctl start chronyd

# 配置NTP源,添加国内公共的NTP服务器
# vim /etc/chrony.conf
server ntp.aliyun.com iburst
server ntp1.tencent.com iburst
server cn.pool.ntp.org iburst

# 重启chrony服务
# systemctl restart chronyd

# 检查chrony服务的运行状态
# systemctl status chronyd

# 查看chrony服务的时间同步状态
# chronyc tracking

# 强制同步一次时间(只在刚部署、时间差很多的时候用)
# chronyc makestep

设置主机名(比如 k8s-master1k8s-master2k8s-node1k8s-node2k8s-node3

1
# hostnamectl set-hostname <hostname>

添加 hosts 配置信息

1
2
3
4
5
6
7
# 添加hosts
# vim /etc/hosts
192.168.2.191 k8s-master1
192.168.2.148 k8s-master2
192.168.2.112 k8s-node1
192.168.2.131 k8s-node2
192.168.2.236 k8s-node3

将桥接的 IPv4 流量传递到 iptables 的链

1
2
3
4
5
6
7
# 创建配置文件,添加相应的路由规则
# vim /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1

# 配置生效
# sysctl --system

安装 CFSSL 工具

特别注意

以下所有操作都是仅在 Kubernetes 集群的任意一个 Master 节点上(比如 k8s-master1)执行。CFSSL 是 CloudFlare 开源的一款 PKI/TLS 工具,包含一个命令行工具和一个用于签名、验证并且捆绑 TLS 证书的 HTTP API 服务,使用 Go 语言编写。

1
2
3
4
5
6
7
8
9
10
# 二进制方式安装CFSSL工具
# curl -L https://pkg.cfssl.org/R1.2/cfssl_linux-amd64 -o /usr/local/bin/cfssl
# curl -L https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64 -o /usr/local/bin/cfssljson
# curl -L https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64 -o /usr/local/bin/cfssl-certinfo

# 文件授权
# chmod +x /usr/local/bin/cfssl*

# 配置环境变量
# export PATH=/usr/local/bin:$PATH

安装 Docker 服务

特别注意

  • 在 Kubernetes 集群的所有节点(包括 Master 和 Node 节点)上分别安装 Docker,这里采用二进制安装方式,CentOS 上还可以通过 YUM 安装
  • 如果 Master 节点仅用于运行控制平面组件(如 API Server、Controller Manager 和 Scheduler),不承载用户 Pod 的调度和运行任务,则 Master 节点可以不安装 Docker 服务。

下载并解压 Docker 的二进制包

1
2
3
4
5
6
7
8
# 下载Docker的二进制包
# wget https://download.docker.com/linux/static/stable/x86_64/docker-19.03.9.tgz

# 解压文件
# tar -xvf docker-19.03.9.tgz

# 移动文件
# mv docker/* /usr/bin

创建 Docker 的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建Docker的配置目录
# mkdir /etc/docker

# 配置阿里的Docker镜像加速(请更换自己的镜像地址,或者使用国内公共的镜像地址)
# cat > /etc/docker/daemon.json << EOF
{
"registry-mirrors": [
"https://b9pmyelo.mirror.aliyuncs.com",
"https://ustc-edu-cn.mirror.aliyuncs.com",
"https://mirror.iscas.ac.cn",
"https://docker.nju.edu.cn",
"https://docker.m.daocloud.io",
"https://ccr.ccs.tencentyun.com",
"https://dockerhub.timeweb.cloud"
]
}
EOF

配置 Systemd 管理 Docker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# cat > /usr/lib/systemd/system/docker.service << EOF
[Unit]
Description=Docker Application Container Engine
Documentation=https://docs.docker.com
After=network-online.target
After=firewalld.service
Wants=network-online.target

[Service]
Type=notify
ExecStart=/usr/bin/dockerd
ExecReload=/bin/kill -s HUP $MAINPID
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
TimeoutStartSec=0
Delegate=yes
KillMode=process
Restart=on-failure
StartLimitBurst=3
StartLimitInterval=60s

[Install]
WantedBy=multi-user.target
EOF

启动并设置开机自启动 Docker 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 更新系统配置
# systemctl daemon-reload

# 开机自启动Docker
# systemctl enable docker

# 启动Docker
# systemctl start docker

# 查看Docker的运行状态
# systemctl status docker

# 查看Docker的版本
# docker --version

# 查看Docker的安装信息
# docker info

搭建 Etcd 集群

Etcd 是一个分布式键值存储系统,Kubernetes 使用 Etcd 进行数据存储,所以先准备一个 Etcd 系统。为解决 Etcd 单点故障,建议采用集群方式部署,这里使用 3 台机器组建 Etcd 集群,可容忍 1 台机器故障。当然,也可以使用 5 台机器组建 Etcd 集群,可容忍 2 台机器故障。为了节省机器,这里复用 Kubernetes 节点的机器,也可以独立于 Kubernetes 集群之外部署,只要 Kubernetes 的 API Server 能够连接上就行。

生成 Etcd 证书

特别注意

  • 以下所有操作都是仅在 Kubernetes 集群的任意一个节点上(比如,第一个 Master 节点)执行,千万不要在每个 Etcd 集群节点上单独生成证书,否则 Etcd 集群里的节点可能会因证书不一致而导致集群启动失败
  • 以后向 Etcd 集群中添加新节点时,只要将对应的证书拷贝到新节点上即可,通过 CFSSL 工具生成证书的详细使用教程请看 这里
  • 这里生成的 Etcd 证书,主要是提供给 API Server 使用的,目的是为了 API Server 与 Etcd 集群之间使用 CA 证书进行加密和身份验证。
组件使用的证书
Etcdca.pemserver-key.pemserver.pem

创建存放 Etcd 证书的目录

1
2
3
4
5
# 创建证书目录
mkdir -p ~/tls/etcd/

# 进入证书目录
cd ~/tls/etcd/

创建 CA 证书的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# cat << EOF | tee ca-config.json
{
"signing": {
"default": {
"expiry": "87600h"
},
"profiles": {
"www": {
"expiry": "87600h",
"usages": [
"signing",
"key encipherment",
"server auth",
"client auth"
]
}
}
}
}
EOF

创建 CA 证书签名的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# cat << EOF | tee ca-csr.json
{
"CN": "etcd ca",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"ST": "BeiJing",
"L": "BeiJing",
"O": "k8s",
"OU": "System"
}
],
"ca": {
"expiry": "87600h"
}
}
EOF

创建用于生成 Etcd 证书的配置文件,如果 hosts 字段不为空,则需要指定授权使用该证书的 IP 或域名列表。由于该证书后续会被 Etcd 集群使用到,因此通常指定为 Etcd 集群所有节点的 IP 集合。因为本文的 Etcd 集群节点和 Kubernetes 的集群节点共同安装在不同虚拟机内(即服务器复用),所以 IP 列表就是 Kubernetes 集群所有节点的 IP 集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# cat << EOF | tee server-csr.json
{
"CN": "etcd",
"hosts": [
"192.168.2.191",
"192.168.2.148",
"192.168.2.112",
"192.168.2.131",
"192.168.2.236"
],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"ST": "BeiJing",
"L": "BeiJing",
"O": "k8s",
"OU": "System"
}
]
}
EOF

生成 CA 证书,并使用自签 CA 签发 Etcd 证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 查看当前目录下的文件
# ls
ca-config.json ca-csr.json server-csr.json

# 生成CA证书
# cfssl gencert -initca ca-csr.json | cfssljson -bare ca -

# 查看生成的CA证书
# ls ca*.pem
ca-key.pem ca.pem

# 使用自签 CA 证书签发 Etcd 的证书,"-profile" 参数的值必须与 `ca-config.json` 配置文件中的值一致
# cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=www server-csr.json | cfssljson -bare server

# 查看生成的 Etcd 证书
# ls server*.pem
server-key.pem server.pem

# 验证生成的 Etcd 证书
# cfssl-certinfo -cert server.pem
# openssl x509 -noout -text -in server.pem

拷贝上面生成的 CA 证书和 Etcd 证书到本地目录

1
2
3
4
5
# 创建存放证书的本地目录
# mkdir -p /opt/etcd/ssl/

# 拷贝 CA 证书和 Etcd 证书到本地目录
# cp ~/tls/etcd/*.pem /opt/etcd/ssl/

拷贝上面生成的 CA 证书和 Etcd 证书到其他 Master 节点里面(提供给 API Server 使用)

1
2
3
4
5
# 在其他 Master 节点上分别创建存放证书的目录
# ssh root@k8s-master2 "mkdir -p /opt/etcd/ssl/"

# 拷贝 CA 证书和 Etcd 证书到其他 Master 节点里面
# scp ~/tls/etcd/*.pem root@k8s-master2:/opt/etcd/ssl/

部署 Etcd 集群

在第一个 Master 节点(k8s-master1)里面安装 Etcd

1
2
3
4
5
6
7
8
9
10
11
# 下载二进制包
# wget https://github.com/etcd-io/etcd/releases/download/v3.4.9/etcd-v3.4.9-linux-amd64.tar.gz

# 创建目录
# mkdir -p /opt/etcd/{bin,cfg,ssl}

# 解压文件
# tar zxvf etcd-v3.4.9-linux-amd64.tar.gz

# 移动文件
# mv etcd-v3.4.9-linux-amd64/{etcd,etcdctl} /opt/etcd/bin/

在第一个 Master 节点(k8s-master1)里面创建 Etcd 的配置文件,这里必须根据实际情况更改 Etcd 节点的 IP、名称,切勿直接拷贝以下配置内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建Etcd的配置文件
# cat > /opt/etcd/cfg/etcd.conf << EOF
#[Member]
ETCD_NAME="etcd-1"
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_PEER_URLS="https://192.168.2.191:2380"
ETCD_LISTEN_CLIENT_URLS="https://192.168.2.191:2379"

#[Cluster]
ETCD_INITIAL_ADVERTISE_PEER_URLS="https://192.168.2.191:2380"
ETCD_ADVERTISE_CLIENT_URLS="https://192.168.2.191:2379"
ETCD_INITIAL_CLUSTER="etcd-1=https://192.168.2.191:2380,etcd-2=https://192.168.2.112:2380,etcd-3=https://192.168.2.131:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"
ETCD_INITIAL_CLUSTER_STATE="new"
EOF
1
2
3
4
5
6
7
8
9
10
# 配置说明:
ETCD_NAME:节点名称,集群中唯一
ETCDDATADIR:数据目录路径
ETCD_LISTEN_PEER_URLS:集群通信监听地址
ETCD_LISTEN_CLIENT_URLS:客户端访问监听地址
ETCD_INITIAL_ADVERTISE_PEER_URLS:集群通告地址
ETCD_ADVERTISE_CLIENT_URLS:客户端通告地址
ETCD_INITIAL_CLUSTER:集群节点地址
ETCD_INITIAL_CLUSTER_TOKEN:集群 Token
ETCD_INITIAL_CLUSTER_STATE:加入集群的当前状态,new 是新集群,existing 表示加入已有集群

在第一个 Master 节点(k8s-master1)里面使用 Systemd 管理 Etcd 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 创建Etcd服务管理的配置文件
# cat > /usr/lib/systemd/system/etcd.service << EOF
[Unit]
Description=Etcd Server
After=network.target
After=network-online.target
Wants=network-online.target

[Service]
Type=notify
EnvironmentFile=/opt/etcd/cfg/etcd.conf
ExecStart=/opt/etcd/bin/etcd \
--cert-file=/opt/etcd/ssl/server.pem \
--key-file=/opt/etcd/ssl/server-key.pem \
--peer-cert-file=/opt/etcd/ssl/server.pem \
--peer-key-file=/opt/etcd/ssl/server-key.pem \
--trusted-ca-file=/opt/etcd/ssl/ca.pem \
--peer-trusted-ca-file=/opt/etcd/ssl/ca.pem \
--logger=zap
Restart=always
RestartSec=10s
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF
1
2
3
4
5
# 更新系统配置
# systemctl daemon-reload

# 开机自启动Etcd服务
# systemctl enable etcd

在第一个 Master 节点(k8s-master1)里面拷贝所有 Etcd 文件到其他 2 个 Node 节点,并在这 2 个 Node 节点里分别更改 Etcd 的配置文件和设置 Etcd 服务开机自启动,最终 Etcd 集群会有 3 个节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 拷贝Etcd的所有文件到各个Node节点
# scp -r /opt/etcd/ root@k8s-node1:/opt/
# scp -r /opt/etcd/ root@k8s-node2:/opt/

# 拷贝Etcd服务管理的配置文件到各个Node节点
# scp /usr/lib/systemd/system/etcd.service root@k8s-node1:/usr/lib/systemd/system/
# scp /usr/lib/systemd/system/etcd.service root@k8s-node2:/usr/lib/systemd/system/

# 在其他2个Node节点里分别编辑Etcd的配置文件,包括更改当前节点的名称("ETCD_NAME")和IP,注意除了 "ETCD_INITIAL_CLUSTER" 配置项里的IP不需要更改,其他IP都需要更改为当前节点的IP
# vim /opt/etcd/cfg/etcd.conf

# 在其他2个Node节点里分别设置Etcd服务开机自启动
# systemctl daemon-reload
# systemctl enable etcd

比如,Etcd 集群第 2 个节点的配置文件(etcd.conf)内容如下:

1
2
3
4
5
6
7
8
9
10
11
ETCD_NAME="etcd-2"
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_PEER_URLS="https://192.168.2.112:2380"
ETCD_LISTEN_CLIENT_URLS="https://192.168.2.112:2379"

#[Cluster]
ETCD_INITIAL_ADVERTISE_PEER_URLS="https://192.168.2.112:2380"
ETCD_ADVERTISE_CLIENT_URLS="https://192.168.2.112:2379"
ETCD_INITIAL_CLUSTER="etcd-1=https://192.168.2.191:2380,etcd-2=https://192.168.2.112:2380,etcd-3=https://192.168.2.131:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"
ETCD_INITIAL_CLUSTER_STATE="new"

比如,Etcd 集群第 3 个节点的配置文件(etcd.conf)内容如下:

1
2
3
4
5
6
7
8
9
10
11
ETCD_NAME="etcd-3"
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_PEER_URLS="https://192.168.2.131:2380"
ETCD_LISTEN_CLIENT_URLS="https://192.168.2.131:2379"

#[Cluster]
ETCD_INITIAL_ADVERTISE_PEER_URLS="https://192.168.2.131:2380"
ETCD_ADVERTISE_CLIENT_URLS="https://192.168.2.131:2379"
ETCD_INITIAL_CLUSTER="etcd-1=https://192.168.2.191:2380,etcd-2=https://192.168.2.112:2380,etcd-3=https://192.168.2.131:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"
ETCD_INITIAL_CLUSTER_STATE="new"

启动 Etcd 集群

在 Etcd 集群的所有节点上分别执行以下命令来启动 Etcd,值得一提的是,必须在多个 Etcd 节点里同时执行 systemctl start etcd 命令来启动 Etcd 集群,否则单个 Etcd 节点是无法正常启动的

1
2
3
4
5
6
7
8
# 启动Etcd服务(多个节点必须同时启动)
# systemctl start etcd

# 查看Etcd服务的运行状态
# systemctl status etcd

# 查看Etcd服务的启动日志(可用于排查启动问题)
# journalctl -u etcd.service

若 Etcd 集群启动成功,可以在任意一个 Etcd 节点里执行以下命令来查看集群的运行状态,比如 Leader 选举结果(请自行更改 Etcd 集群节点的 IP 和端口,切勿直接拷贝执行以下命令

1
2
# 查看Etcd集群的运行状态
# /opt/etcd/bin/etcdctl --endpoints=https://192.168.2.191:2379,https://192.168.2.112:2379,https://192.168.2.131:2379 --cacert=/opt/etcd/ssl/ca.pem --cert=/opt/etcd/ssl/server.pem --key=/opt/etcd/ssl/server-key.pem endpoint status --write-out=table
1
2
3
4
5
6
7
+----------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS |
+----------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| https://192.168.2.191:2379 | 9a8c44f9310511 | 3.4.9 | 442 kB | true | false | 2 | 519 | 519 | |
| https://192.168.2.112:2379 | a00042f5519886e3 | 3.4.9 | 451 kB | false | false | 2 | 519 | 519 | |
| https://192.168.2.131:2379 | 9cb4eb07d38510b5 | 3.4.9 | 446 kB | false | false | 2 | 519 | 519 | |
+----------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+

若 Etcd 集群启动失败,可以在所有的 Etcd 节点里分别执行以下操作来解决

1
2
3
4
5
6
7
8
9
10
11
# 关闭Etcd服务
# systemctl stop etcd

# 删除Etcd服务的数据目录
# rm -rf /var/lib/etcd/default.etcd

# 重启Etcd服务(多个Etcd节点必须同时重启)
# systemctl start etcd

# 查看Etcd服务的运行状态
# systemctl status etcd

特别注意

Etcd 集群使用了 Raft 协议,很多操作要依赖时间戳和进行超时判断,因此必须安装类似 chrony 这样的工具来同步操作系统的时间,否则可能会影响 Etcd 集群的正常工作。

搭建 Kubernetes 集群

生成集群证书

特别注意

  • 以下所有操作都是仅在 Kubernetes 集群的任意一个 Master 节点(比如 k8s-master1)上执行,千万不要在每个 Kubernetes 集群节点上单独生成 API Server 或者 Kube-Proxy 等证书,否则可能会因证书不一致而导致 Kubernetes 集群启动失败
  • 简而言之,所有证书(包括 API Server、Kube-Proxy 等证书)都是在任意一个 Master 节点上面生成,其他 Kubernetes 集群节点不参与生成证书。以后向 Kubernetes 集群中添加任何新节点时,只要将对应的证书拷贝到新节点上即可
  • 值得一提的是,通过 CFSSL 工具生成证书的详细使用教程请看 这里
组件使用的证书
CA 根证书ca.pemca-key.pem
API Serverca.pemkube-apiserver-key.pemkube-apiserver.pem
Controller Managerca.pemkube-controller-manager-key.pemkube-controller-manager.pem
Schedulerca.pemkube-scheduler-key.pemkube-scheduler.pem
Kubeletca.pem
Kube-Proxyca.pemkube-proxy-key.pemkube-proxy.pem
生成 CA 根证书

创建存放证书的目录

1
2
# 这里为了避免与上面生成的 Etcd 证书混淆,创建一个新的证书目录,然后在该目录下存放 Kubernetes 集群所需的证书
# mkdir -p ~/tls/k8s/

进入存放证书的目录

1
2
# 进入证书目录
# cd ~/tls/k8s/

创建 CA 证书的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# cat > ca-config.json << EOF
{
"signing": {
"default": {
"expiry": "87600h"
},
"profiles": {
"kubernetes": {
"expiry": "87600h",
"usages": [
"signing",
"key encipherment",
"server auth",
"client auth"
]
}
}
}
}
EOF

创建 CA 证书签名的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# cat > ca-csr.json << EOF
{
"CN": "kubernetes",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"L": "Beijing",
"ST": "Beijing",
"O": "k8s",
"OU": "System"
}
]
}
EOF

生成 CA 证书

1
2
3
4
5
6
7
8
9
10
# 生成CA证书
# cfssl gencert -initca ca-csr.json | cfssljson -bare ca -

# 查看生成的CA证书
# ls ca*.pem
ca-key.pem ca.pem

# 验证生成的CA证书
# cfssl-certinfo -cert ca.pem
# openssl x509 -noout -text -in ca.pem

将上面生成的 CA 证书拷贝到本地目录里面

1
2
3
4
5
# 创建存放证书的本地目录
# mkdir -p /opt/kubernetes/ssl/

# 拷贝 CA 证书到本地目录
# cp ~/tls/k8s/ca*.pem /opt/kubernetes/ssl/

将上面生成的 CA 证书拷贝到其他 Master 节点里面

1
2
3
4
5
# 在其他 Master 节点上分别创建存放证书的目录
# ssh root@k8s-master2 "mkdir -p /opt/kubernetes/ssl/"

# 拷贝 CA 证书到其他 Master 节点里面
# scp ~/tls/k8s/ca*.pem root@k8s-master2:/opt/kubernetes/ssl/

将上面生成的 CA 证书拷贝到所有 Node 节点里面

1
2
3
4
5
6
7
8
9
# 在所有 Node 节点上分别创建存放证书的目录
# ssh root@k8s-node1 "mkdir -p /opt/kubernetes/ssl/"
# ssh root@k8s-node2 "mkdir -p /opt/kubernetes/ssl/"
# ssh root@k8s-node3 "mkdir -p /opt/kubernetes/ssl/"

# 拷贝 CA 证书到所有 Node 节点里面
# scp ~/tls/k8s/ca*.pem root@k8s-node1:/opt/kubernetes/ssl/
# scp ~/tls/k8s/ca*.pem root@k8s-node2:/opt/kubernetes/ssl/
# scp ~/tls/k8s/ca*.pem root@k8s-node3:/opt/kubernetes/ssl/
生成 API Server 证书

进入存放证书的目录

1
2
# 进入证书目录
# cd ~/tls/k8s/

创建用于生成 API Server 证书的配置文件,如果 hosts 字段不为空,需要指定授权使用该证书的 IP 或域名列表。由于该证书会被 Kubernetes 集群使用到,通常需要包含 Kubernetes 集群所有节点的 IP,以及 Kubernetes 服务的 Cluster IP(即 kube-apiserver.conf 配置文件中指定的 --service-cluster-ip-range 网段的第一个可用 IP,例如 10.0.0.1,具体取决于 Kubernetes 集群的 Service 网段配置),最后还要加上 Keepalived 的虚拟 IP 地址(比如 192.168.2.100

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# cat << EOF | tee kube-apiserver-csr.json
{
"CN": "kubernetes",
"hosts": [
"10.0.0.1",
"127.0.0.1",
"192.168.2.191",
"192.168.2.148",
"192.168.2.112",
"192.168.2.131",
"192.168.2.236",
"192.168.2.100",
"kubernetes",
"kubernetes.default",
"kubernetes.default.svc",
"kubernetes.default.svc.cluster",
"kubernetes.default.svc.cluster.local"
],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"ST": "BeiJing",
"L": "BeiJing",
"O": "k8s",
"OU": "System"
}
]
}
EOF

使用自签 CA 签发 API Server 证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看当前目录下的文件
# ls
ca-config.json ca.csr ca-csr.json ca-key.pem ca.pem kube-apiserver-csr.json

# 使用自签 CA 证书签发 API Server 的 HTTPS 证书,"-profile" 参数的值必须与 `ca-config.json` 配置文件中的值一致
# cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kube-apiserver-csr.json | cfssljson -bare kube-apiserver

# 查看生成的 API Server 证书
# ls kube-apiserver*.pem
kube-apiserver-key.pem kube-apiserver.pem

# 验证生成的 API Server 证书
# cfssl-certinfo -cert kube-apiserver.pem
# openssl x509 -noout -text -in kube-apiserver.pem

将上面生成的 API Server 证书拷贝到本地目录里面

1
2
# 拷贝 API Server 证书到本地目录
# cp ~/tls/k8s/kube-apiserver*.pem /opt/kubernetes/ssl/

将上面生成的 API Server 证书拷贝到其他 Master 节点里面

1
2
# 拷贝 API Server 证书到其他 Master 节点里面
# scp ~/tls/k8s/kube-apiserver*.pem root@k8s-master2:/opt/kubernetes/ssl/
生成 Controller Manager 证书

进入存放证书的目录

1
2
# 进入证书目录
# cd ~/tls/k8s/

创建用于生成 Controller Manager 证书的配置文件,hosts 必须包含 127.0.0.1、所有 Master 节点的 IP、Keepalived 的虚拟 IP 地址(比如 192.168.2.100),请自行更改,切勿直接拷贝使用以下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# cat > kube-controller-manager-csr.json << EOF
{
"CN": "system:kube-controller-manager",
"hosts": [
"127.0.0.1",
"192.168.2.191",
"192.168.2.148",
"192.168.2.100"
],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"ST": "Beijing",
"L": "Beijing",
"O": "system:kube-controller-manager",
"OU": "System"
}
]
}
EOF

使用自签 CA 签发 Controller Manager 证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看当前目录下的文件
# ls
ca-config.json ca.csr ca-csr.json ca-key.pem ca.pem kube-apiserver.csr kube-apiserver-csr.json kube-apiserver-key.pem kube-apiserver.pem kube-controller-manager-csr.json

# 使用自签 CA 证书签发 Controller Manager 证书,"-profile" 参数的值必须与 `ca-config.json` 配置文件中的值一致
# 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

# 查看生成的 Controller Manager 证书
# ls kube-controller-manager*.pem
kube-controller-manager-key.pem kube-controller-manager.pem

# 验证生成的 Controller Manager 证书
# cfssl-certinfo -cert kube-controller-manager.pem
# openssl x509 -noout -text -in kube-controller-manager.pem

将上面生成的 Controller Manager 证书拷贝到本地目录里面

1
2
# 拷贝 Controller Manager 证书到本地目录
# cp ~/tls/k8s/kube-controller-manager*.pem /opt/kubernetes/ssl/

将上面生成的 Controller Manager 证书拷贝到其他 Master 节点里面

1
2
# 拷贝 Controller Manager 证书到其他 Master 节点里面
# scp ~/tls/k8s/kube-controller-manager*.pem root@k8s-master2:/opt/kubernetes/ssl/
生成 Scheduler 证书

进入存放证书的目录

1
2
# 进入证书目录
# cd ~/tls/k8s/

创建用于生成 Scheduler 证书的配置文件,hosts 必须包含 127.0.0.1、所有 Master 节点的 IP、Keepalived 的虚拟 IP 地址(比如 192.168.2.100),请自行更改,切勿直接拷贝使用以下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# cat > kube-scheduler-csr.json << EOF
{
"CN": "system:kube-scheduler",
"hosts": [
"127.0.0.1",
"192.168.2.191",
"192.168.2.148",
"192.168.2.100"
],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"ST": "BeiJing",
"L": "BeiJing",
"O": "system:kube-scheduler",
"OU": "System"
}
]
}
EOF

使用自签 CA 签发 Scheduler 证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看当前目录下的文件
# ls
ca-config.json ca.csr ca-csr.json ca-key.pem ca.pem kube-apiserver.csr kube-apiserver-csr.json kube-apiserver-key.pem kube-apiserver.pem kube-controller-manager.csr kube-controller-manager-csr.json kube-controller-manager-key.pem kube-controller-manager.pem kube-scheduler-csr.json

# 使用自签 CA 证书签发 Scheduler 证书,"-profile" 参数的值必须与 `ca-config.json` 配置文件中的值一致
# cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kube-scheduler-csr.json | cfssljson -bare kube-scheduler

# 查看生成的 Scheduler 证书
# ls kube-scheduler*.pem
kube-scheduler-key.pem kube-scheduler.pem

# 验证生成的 Scheduler 证书
# cfssl-certinfo -cert kube-scheduler.pem
# openssl x509 -noout -text -in kube-scheduler.pem

将上面生成的 Scheduler 证书拷贝到本地目录里面

1
2
# 拷贝 Scheduler 证书到本地目录
# cp ~/tls/k8s/kube-scheduler*.pem /opt/kubernetes/ssl/

将上面生成的 Scheduler 证书拷贝到其他 Master 节点里面

1
2
# 拷贝 Scheduler 证书到其他 Master 节点里面
# scp ~/tls/k8s/kube-scheduler*.pem root@k8s-master2:/opt/kubernetes/ssl/
生成 Kube-Proxy 证书

进入存放证书的目录

1
2
# 进入证书目录
# cd ~/tls/k8s/

创建用于生成 Kube-Proxy 证书的配置文件,hosts 配置项的值可以为空,因为 Kube-Proxy 不直接被访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# cat > kube-proxy-csr.json << EOF
{
"CN": "system:kube-proxy",
"hosts": [],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"L": "Beijing",
"ST": "Beijing",
"O": "system:node-proxier",
"OU": "System"
}
]
}
EOF

使用自签 CA 签发 Kube-Proxy 证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 查看当前目录下的文件
# ls
ca-config.json ca-csr.json ca.pem kube-apiserver-csr.json kube-apiserver.pem kube-controller-manager-csr.json kube-controller-manager.pem kube-scheduler.csr kube-scheduler-key.pem
ca.csr ca-key.pem kube-apiserver.csr kube-apiserver-key.pem kube-controller-manager.csr kube-controller-manager-key.pem kube-proxy-csr.json kube-scheduler-csr.json kube-scheduler.pem

# 使用自签 CA 证书签发 Kube-Proxy 证书,"-profile" 参数的值必须与 `ca-config.json` 配置文件中的值一致
# cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kube-proxy-csr.json | cfssljson -bare kube-proxy

# 查看生成的 Kube-Proxy 证书
# ls kube-proxy*.pem
kube-proxy-key.pem kube-proxy.pem

# 验证生成的 Kube-Proxy 证书
# cfssl-certinfo -cert kube-proxy.pem
# openssl x509 -noout -text -in kube-proxy.pem

将上面生成的 Kube-Proxy 证书拷贝到本地目录里面

1
2
# 拷贝 Kube-Proxy 证书到本地目录
# cp ~/tls/k8s/kube-proxy*.pem /opt/kubernetes/ssl/

将上面生成的 Kube-Proxy 证书拷贝到其他 Master 节点里面

1
2
# 拷贝 Kube-Proxy 证书到其他 Master 节点里面
# scp ~/tls/k8s/kube-proxy*.pem root@k8s-master2:/opt/kubernetes/ssl/

将上面生成的 Kube-Proxy 证书拷贝到所有 Node 节点里面

1
2
3
4
# 拷贝 Kube-Proxy 证书到所有 Node 节点里面
# scp ~/tls/k8s/kube-proxy*.pem root@k8s-node1:/opt/kubernetes/ssl/
# scp ~/tls/k8s/kube-proxy*.pem root@k8s-node2:/opt/kubernetes/ssl/
# scp ~/tls/k8s/kube-proxy*.pem root@k8s-node3:/opt/kubernetes/ssl/

单独部署 Master 节点

特别注意

以下所有操作都是仅在 Kubernetes 集群的两台 Master 节点上执行,也就是分别在两台 Master 节点上部署 API Server、Controller Manager、Scheduler、HaProxy、Keepalived

下载二进制文件

下载地址

  • Kubernetes 相关的二进制包可以从 GitHub 下载得到,这里下载的版本是 v1.19.10
  • 当打开 GitHub 链接后,会发现里面有很多二进制包,只需要下载一个 Server 二进制包就够了,里面包含了 Master 节点相关的二进制包。

下载并解压二进制包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 下载二进制包(耗时较长)
# wget https://dl.k8s.io/v1.19.10/kubernetes-server-linux-amd64.tar.gz

# 创建目录
# mkdir -p /opt/kubernetes/{bin,cfg,ssl,logs}

# 解压文件
# tar zxvf kubernetes-server-linux-amd64.tar.gz

# 进入解压目录
# cd kubernetes/server/bin

# 拷贝文件
# cp kube-apiserver kube-scheduler kube-controller-manager /opt/kubernetes/bin/

# 拷贝文件
# cp kubectl /usr/bin/
部署 API Server

创建 API Server 的配置文件,使用转义符 \\ 是为了使 EOF 保留换行符。请自行更改 --bind-address--advertise-address 的值为当前 Master 节点的 IP 地址(比如 192.168.2.191),而 --etcd-servers 需要改为 Etcd 所有集群节点的 IP 地址,其他配置可以不更改,切勿直接拷贝以下配置内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 创建配置文件
# cat > /opt/kubernetes/cfg/kube-apiserver.conf << EOF
KUBE_APISERVER_OPTS="--logtostderr=false \\
--v=2 \\
--log-dir=/opt/kubernetes/logs \\
--etcd-servers=https://192.168.2.191:2379,https://192.168.2.112:2379,https://192.168.2.131:2379 \\
--bind-address=<当前节点的IP地址> \\
--secure-port=6443 \\
--advertise-address=<当前节点的IP地址> \\
--allow-privileged=true \\
--service-cluster-ip-range=10.0.0.0/24 \\
--enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,ResourceQuota,NodeRestriction \\
--authorization-mode=RBAC,Node \\
--enable-bootstrap-token-auth=true \\
--token-auth-file=/opt/kubernetes/cfg/token.csv \\
--service-node-port-range=30000-32767 \\
--kubelet-client-certificate=/opt/kubernetes/ssl/kube-apiserver.pem \\
--kubelet-client-key=/opt/kubernetes/ssl/kube-apiserver-key.pem \\
--tls-cert-file=/opt/kubernetes/ssl/kube-apiserver.pem \\
--tls-private-key-file=/opt/kubernetes/ssl/kube-apiserver-key.pem \\
--client-ca-file=/opt/kubernetes/ssl/ca.pem \\
--service-account-key-file=/opt/kubernetes/ssl/ca-key.pem \\
--etcd-cafile=/opt/etcd/ssl/ca.pem \\
--etcd-certfile=/opt/etcd/ssl/server.pem \\
--etcd-keyfile=/opt/etcd/ssl/server-key.pem \\
--audit-log-maxage=30 \\
--audit-log-maxbackup=3 \\
--audit-log-maxsize=100 \\
--audit-log-path=/opt/kubernetes/logs/k8s-audit.log"
EOF

配置参数说明

参数含义
--logtostderr启用将日志输出到 stderr(通常设为 false 以输出到指定目录)。
--v日志详细级别(verbosity level),数值越大输出越详细。
--log-dir日志文件输出目录。
--etcd-serversEtcd 集群地址列表(用逗号分隔,支持 HTTPS)。
--bind-addressKube-ApiServer 监听的 IP 地址,通常是 Master 节点的 IP 地址。
--secure-portHTTPS 安全端口(默认 6443)。
--advertise-address集群中对外通告的地址(其他组件访问时使用),通常是 Master 节点的 IP 地址。
--allow-privileged是否允许特权容器(启用授权相关特性)。
--service-cluster-ip-rangeService 虚拟 IP 地址段,用于分配 Cluster IP。
--enable-admission-plugins启用的准入控制插件列表(例如 NamespaceLifecycle, LimitRanger 等)。
--authorization-mode认证 / 授权模式(如 RBAC,Node 表示启用 RBAC 和节点自管理)。
--enable-bootstrap-token-auth启用基于 Bootstrap Token 的 TLS 引导认证。
--token-auth-fileBootstrap Token 的静态文件路径(比如 token.csv)。
--service-node-port-rangeNodePort 类型 Service 默认分配的端口范围(如 30000-32767)。
--kubelet-client-certificateKube-ApiServer 访问 kubelet 时使用的客户端证书路径。
--kubelet-client-keyKube-ApiServer 访问 kubelet 时使用的客户端私钥路径。
--tls-cert-fileKube-ApiServer HTTPS 使用的证书文件。
--tls-private-key-fileKube-ApiServer HTTPS 使用的私钥文件。
--client-ca-file用于验证客户端证书(如 kubelet/client)的 CA 证书。
--service-account-key-file用于签名 Service Account Token 的私钥文件。
--etcd-cafile连接 Etcd 时验证其证书的 CA 证书路径。
--etcd-certfileKube-ApiServer 连接 Etcd 时使用的客户端证书。
--etcd-keyfileKube-ApiServer 连接 Etcd 时使用的客户端私钥。
--audit-log-maxage审计日志保留的最大天数。
--audit-log-maxbackup审计日志保留的备份数量。
--audit-log-maxsize单个审计日志文件的最大大小(MB)。
--audit-log-path审计日志输出路径。

TLS Bootstraping 机制介绍

在 Master 节点中,API Server 启用 TLS 认证后,Node 节点上的 kubelet 和 kube-proxy 必须使用由 CA 签发的有效客户端证书才能与 kube-apiserver 通信。当 Node 节点数量较多时,手动颁发这些客户端证书的工作量大,且会增加集群扩展的复杂度。为简化这一流程,Kubernetes 引入了 TLS Bootstrapping 机制:kubelet 会以低权限用户身份向 API Server 自动申请客户端证书,并由 API Server 动态签发,从而实现证书的自动化管理。TLS Bootstrapping 机制的工作流程如图所示

创建在 API Server 配置文件(kube-apiserver.conf)中通过 --token-auth-file 指定的 Token 文件,用于配置 TLS Bootstrapping 机制

1
2
3
4
# 创建Token文件
# cat > /opt/kubernetes/cfg/token.csv << EOF
c47ffb939f5ca36231d9e3121a252940,kubelet-bootstrap,10001,"system:node-bootstrapper"
EOF

Token 文件的数据格式是 Token,用户名,UID,用户组,其中 UID 仅作标识使用,可以是任意唯一值;用户组 system:node-bootstrapper 是 Kubelet TLS Bootstrap 必需的组;Token 可以使用以下命令自行生成并替换掉

1
2
# 生成Token
# head -c 16 /dev/urandom | od -An -t x | tr -d ' '

配置 Systemd 管理 API Server 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建 API Server 服务管理的配置文件
# cat > /usr/lib/systemd/system/kube-apiserver.service << EOF
[Unit]
Description=Kubernetes API Server
Documentation=https://github.com/kubernetes/kubernetes
After=network-online.target

[Service]
EnvironmentFile=/opt/kubernetes/cfg/kube-apiserver.conf
ExecStart=/opt/kubernetes/bin/kube-apiserver \$KUBE_APISERVER_OPTS
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF

启动并设置开机自启动 API Server 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 更新系统配置
# systemctl daemon-reload

# 开机自启动 API Server 服务
# systemctl enable kube-apiserver

# 启动 API Server 服务
# systemctl start kube-apiserver

# 查看 API Server 服务的运行状态
# systemctl status kube-apiserver

# 查看 API Server 服务的启动日志(可用于排查启动问题)
# journalctl -u kube-apiserver.service

授权 kubelet-bootstrap 用户允许请求(申请)证书,请注意,无论有多少个 Master 节点,都只需执行一次授权,重复执行授权会提示失败

1
# kubectl create clusterrolebinding kubelet-bootstrap --clusterrole=system:node-bootstrapper --user=kubelet-bootstrap
部署 HaProxy

特别注意

  • 在两台 Master 节点上,分别安装 HaProxy,两台 Master 节点上的 HaProxy 配置文件都是一样的
  • 生产级 Kubernetes 高可用集群通常采用 HAProxy + Keepalived 实现 API Server 的负载均衡(高性能四层负载均衡),而不是使用 Nginx + Keepalived 替代(可选)。同时,会结合使用 Nginx Ingress 处理应用流量负载,两者分层清晰、运维简单

1. 安装概述

HAProxy 是一款广泛使用的反向代理与负载均衡软件,可以采用其四层(TCP)模式对 Kubernetes API Server 进行负载均衡,从而实现多 Master 节点的高可用访问。通过 HAProxy 构建 API Server 的高可用架构,其整体设计如下图所示:

安装说明路径
HaProxy 的安装路径/usr/local/sbin/haproxy
HaProxy 的配置文件/etc/haproxy/haproxy.cfg
HaProxy 的 PID 文件/var/lib/haproxy/haproxy.pid
HaProxy 的 Socket 文件/var/lib/haproxy/stats
HaProxy 的 Systemd 服务配置文件/usr/lib/systemd/system/haproxy.service

2. 安装步骤

安装依赖软件包

1
2
# 安装软件包
# yum install -y gcc make pcre-devel openssl openssl-devel systemd-devel wget

下载 HaProxy 源码包(官网下载地址

1
2
# 下载源码包
# wget https://www.haproxy.org/download/2.8/src/haproxy-2.8.5.tar.gz

下载 HaProxy 源码包

1
2
# 解压源码包
# tar -xvf haproxy-2.8.5.tar.gz

编译源码与安装

1
2
3
4
5
6
7
8
9
10
11
12
13
# 进入源码包的解压目录
# cd haproxy-2.8.5

# 编译源码
# make TARGET=linux-glibc \
USE_OPENSSL=1 \
USE_PCRE=1 \
USE_SYSTEMD=1 \
USE_ZLIB=1 \
-j$(nproc)

# 安装
# make install

3. 创建用户

创建 HaProxy 用户与用户组

1
2
3
4
5
# 创建用户组
# groupadd --system haproxy

# 创建用户(禁止登录)
# useradd --system --no-create-home --shell /sbin/nologin --gid haproxy haproxy

4. 配置服务

配置 Systemd 管理 HaProxy 服务

1
2
# 创建系统配置文件,添加以下配置内容
# vim /usr/lib/systemd/system/haproxy.service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Unit]
Description=HAProxy Load Balancer
After=network.target

[Service]
User=haproxy
Group=haproxy
LimitNOFILE=1048576
Environment="CONFIG=/etc/haproxy/haproxy.cfg" "PIDFILE=/var/lib/haproxy/haproxy.pid"
ExecStartPre=/usr/local/sbin/haproxy -f $CONFIG -c -q
ExecStart=/usr/local/sbin/haproxy -Ws -f $CONFIG -p $PIDFILE
ExecReload=/bin/kill -USR2 $MAINPID
KillMode=mixed
Type=notify

[Install]
WantedBy=multi-user.target

创建 HaProxy 的运行目录

1
2
3
4
5
6
7
8
# 创建运行目录
# mkdir -p /var/lib/haproxy

# 更改运行目录的所有者
# chown -R haproxy:haproxy /var/lib/haproxy

# 设置运行目录的权限
# chmod 750 /var/lib/haproxy

创建 HaProxy 的配置目录

1
2
# 创建配置目录
# mkdir -p /etc/haproxy

创建 HaProxy 的配置文件

1
2
# 创建配置文件,添加以下配置内容
# vim /etc/haproxy/haproxy.cfg
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#---------------------------------------------------------------------
# Global settings
#---------------------------------------------------------------------
global
# to have these messages end up in /var/log/haproxy.log you will
# need to:
# 1) configure syslog to accept network log events. This is done
# by adding the '-r' option to the SYSLOGD_OPTIONS in
# /etc/sysconfig/syslog
# 2) configure local2 events to go to the /var/log/haproxy.log
# file. A line like the following can be added to
# /etc/sysconfig/syslog
#
# local2.* /var/log/haproxy.log
#
log 127.0.0.1 local2

# chroot /var/lib/haproxy
# pidfile /var/run/haproxy.pid
# user haproxy
# group haproxy
maxconn 4000
nbthread 4
# nbproc 1
# daemon

# turn on stats unix socket
stats socket /var/lib/haproxy/stats

#---------------------------------------------------------------------
# common defaults that all the 'listen' and 'backend' sections will
# use if not designated in their block
#---------------------------------------------------------------------
defaults
mode http
log global
option httplog
option dontlognull
option http-server-close
option redispatch
retries 3
timeout http-request 10s
timeout queue 1m
timeout connect 10s
timeout client 1m
timeout server 1m
timeout http-keep-alive 10s
timeout check 10s
maxconn 3000

#---------------------------------------------------------------------
# kubernetes apiserver frontend which proxys to the backends
#---------------------------------------------------------------------
frontend kubernetes-apiserver
mode tcp
bind *:16443
option tcplog
default_backend kubernetes-apiserver

#---------------------------------------------------------------------
# round robin balancing between the various backends
#---------------------------------------------------------------------
backend kubernetes-apiserver
mode tcp
balance roundrobin
server master01.k8s.io 192.168.2.191:6443 check
server master02.k8s.io 192.168.2.148:6443 check

#---------------------------------------------------------------------
# collection haproxy statistics message
#---------------------------------------------------------------------
listen stats
bind *:1080
stats auth admin:admin
stats refresh 5s
stats realm HAProxy\ Statistics
stats uri /admin?stats

HaProxy 配置文件的使用说明

配置项类型默认值说明
maxconn全局 / 前端 / 后端2000最大连接数,控制 HAProxy 可以同时处理的并发连接数。
nbthread全局1指定每个进程使用的线程数。多线程模式可提高单进程并发性能。
nbproc全局1指定 HAProxy 启动的进程数。多进程模式用于多核优化,但 Systemd 下不推荐与 daemon 配合使用。
daemon全局禁用是否后台运行。Systemd 管理 HaProxy 时通常禁用,HAProxy 由 Systemd 控制前后台。
modedefaults/frontend/backendhttp连接模式,可选 httptcp。可以使用 tcp 来实现 Kubernetes API Server 代理。
balancebackendroundrobin负载均衡算法,可选 roundrobinleastconnsource 等。
stats authlisten 设置统计页面认证用户名和密码,例如 admin:admin
stats refreshlisten10s统计页面刷新时间,单位秒。
stats realmlistenHTTP Basic Auth 的 realm 名称,显示在认证弹窗中。
stats urilisten/HaProxy 统计页面访问 URI,例如 /admin?stats

Haproxy 的配置建议

  • maxconn + nbthread 可以提升单进程并发性能。
  • mode tcp 是代理 Kubernetes API Server 的标准配置,因为 API Server 是 HTTPS / TCP 流量。
  • 当使用 Systemd 管理 HaProxy 时,建议配置 nbproc = 1 + nbthread > 1 + 不启用 daemon,这是最稳定和高效的配置方式。

5. 启动服务

设置开机自启动 HaProxy 服务

1
2
3
4
5
# 更新系统配置
# systemctl daemon-reload

# 开机自启动HaProxy
# systemctl enable haproxy

立刻启动 HaProxy 服务

1
2
3
4
5
# 启动HaProxy
# systemctl start haproxy

# 查看HaProxy的运行状态
# systemctl status haproxy

当 HaProxy 的配置文件发生变更后,可以执行热加载(也叫平滑重载,不会中断现有连接),让配置文件生效

1
2
# 热加载HaProxy配置文件
# systemctl reload haproxy

6. 验证服务

通过 HaProxy 的 16443 反向代理端口(可自定义),验证 HaProxy 的反向代理功能是否可以工作,正常情况下 API Server 的健康检测接口会返回字符串 ok

1
2
# 验证 HaProxy 的反向代理功能(注意,这里即使测试通过,只能说明网络连通性没问题,不能说明其他组件一定可以跟 API Server 通过 HTTPS 协议进行加密通信,尤其是涉及到证书问题时)
# curl --cacert /opt/kubernetes/ssl/ca.pem https://127.0.0.1:16443/healthz

浏览器通过 http://<master-ip>:1080/admin?stats 访问 HaProxy 的统计页面,默认的登录用户名和密码是 admin / admin;如果可以正常访问(如下图所示),则说明 HaProxy 可以正常运行

部署 Keepalived

特别注意

  • 在两台 Master 节点上,分别安装 Keepalived,两台 Master 节点上的 Keepalived 配置文件是不一样的

1. 安装概述

  • Keepalived 是一个高可用软件,基于 VIP 绑定实现服务器双机主备(也叫双机热备),在上述拓扑中,Keepalived 主要根据 HaProxy 的运行状态判断是否需要故障转移(VIP 切换)。
  • 例如,当 HaProxy 主节点挂掉,VIP 会自动绑定在 HaProxy 备节点,从而保证 VIP 一直可用,实现 HaProxy 高可用。
安装说明路径
Keepalived 的安装路径/usr/local/keepalived
Keepalived 的配置文件/usr/local/keepalived/etc/keepalived/keepalived.conf
Keepalived 的 Systemd 服务配置文件(自动生成)/usr/lib/systemd/system/keepalived.service
Keepalived 检测 HaProxy 是否可用的 Shell 脚本/usr/local/keepalived/etc/keepalived/scripts/haproxy_check.sh

2. 安装步骤

安装依赖软件包

1
2
# 安装依赖包
# yum install -y gcc gcc-c++ make openssl-devel libnl libnl-devel net-snmp-devel popt-devel

下载 Keepalived 源码包(官网下载地址

1
2
# 下载源码包
# wget https://www.keepalived.org/software/keepalived-2.2.8.tar.gz

解压 Keepalived 源码包

1
2
# 解压源码包
# tar -xvf keepalived-2.2.8.tar.gz

编译源码与安装

1
2
3
4
5
6
7
8
9
10
11
# 进入源码包的解压目录
# cd keepalived-2.2.8

# 生成构建文件(Makefile)
# ./configure --prefix=/usr/local/keepalived

# 编译源码
# make -j$(nproc)

# 安装
# make install

3. 创建脚本

创建用于 Keepalived 检测 HaProxy 服务是否可用的 Shell 脚本(比如 haproxy_check.sh

1
2
3
4
5
# 创建存放检测脚本的目录
# mkdir -p /usr/local/keepalived/etc/keepalived/scripts/

# 创建检测脚本,并写入以下脚本内容
# vim /usr/local/keepalived/etc/keepalived/scripts/haproxy_check.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash

# 获取HaProxy进程数量
HAPROXY_COUNT=$(pgrep -x haproxy | wc -l)

# 如果没有HaProxy进程,则返回非 0,Keepalived 会认为节点失效
if [ "$HAPROXY_COUNT" -eq 0 ]; then
# 这里可以考虑发送告警邮件
echo "HAProxy not running"
exit 1
else
echo "HAProxy running"
exit 0
fi

授予 HaProxy 检测脚本可执行权限

1
2
# 脚本授权
# chmod +x /usr/local/keepalived/etc/keepalived/scripts/haproxy_check.sh

4. 配置服务

Keepalived 配置文件的使用说明

配置项示例值说明
router_idk8s本节点唯一标识,用于日志区分(不同节点可不同)。
script"killall -0 haproxy"检测 HAProxy 服务是否可用,可以是 Shell 命令,还可以是 Shell 脚本的绝对路径。Keepalived 会根据脚本的返回状态码(0 表示正常,非 0 表示不正常)判断是否调节点整权重或触发 VIP 切换。
interval2检测脚本执行间隔(秒),指定 Keepalived 多久执行一次健康检查脚本。
weight-100节点权重变化值,检测脚本失败时降低优先级或成功时增加优先级。负值表示降低权重,正值表示增加权重。
fall3连续失败次数阈值,当检测脚本连续失败次数达到 fall 时,节点会认为服务不可用,并扣减权重或触发 VIP 切换。
rise2连续成功次数阈值,当检测脚本连续成功次数达到 rise 时,节点会认为服务恢复,并恢复权重初始值。
stateMASTER节点初始角色(MASTERBACKUP),最终由优先级决定。
interfaceenp0s3VRRP 使用的网络接口名称,可通过 ifconfig 命令得知。
virtual_router_id51VRRP 组 ID,多个节点必须一致。
priority200节点优先级(权重),数值越大优先级越高。
advert_int1VRRP 心跳间隔(秒),默认 1 秒。
auth_typePASSVRRP 认证方式(PASSAH)。
auth_passceb1b3ec013d66163d6abVRRP 认证密码,多个节点必须一致。
virtual_ipaddress192.168.2.100Keepalived 提供的 VIP(虚拟 IP),由 MASTER 持有。
track_scriptcheck_haproxy指定用于健康检查的脚本,检测失败时会调节点整权重或触发 VIP 切换。

Keepalived 什么时候触发 VIP 切换

  • Keepalived 的 VIP 切换依赖节点权重变化,只有当 MASTER 节点的实时权重下降到低于 BACKUP 节点时,VIP 才会切换到权重更高的节点。
  • 在 Keepalived 配置文件中,vrrp_script 可以通过 weight 动态调整节点权重:正值会增加节点权重,负值会降低节点权重。
  • 当 Keepalived 脚本检测失败时,节点权重会根据 weight 设置下降,如果下降后 MASTER 节点的权重低于 BACKUP 节点,VIP 就会切换。
  • 当 Keepalived 脚本检测成功时,且连续检测成功的次数达到 rise 次后,节点权重会恢复为初始值,原 MASTER 节点可能会重新成为 MASTER
  • Keepalived 的 weight 必须设置足够大,否则即使 Keepalived 脚本检测失败,MASTER 节点的权重仍可能比 BACKUP 节点高,从而导致不会触发 VIP 切换。

Keepalived 调整节点权重的细节

  • Keepalived 不会因为检测脚本持续失败而反复扣减节点权重,只有当检测脚本的检测结果从 "成功" 变为 "失败" 时,才会触发一次节点权重扣减。
  • 也就是说,第一次检测被判定为失败,会执行一次 weight 扣减;当后续检测脚本继续失败时,由于节点已经处于失败状态,不会再重复扣减节点权重。
  • 当检测脚本恢复成功后,Keepalived 会依据 rise 参数进行节点权重恢复。当检测脚本连续成功达到 rise 次后,节点权重会恢复到初始值。
  • 简而言之,Keepalived 的节点权重扣减是一次性的,直到检测脚本恢复成功,才再次生效。同样的,在节点处于健康状态期间,检测脚本即便持续成功,也不会重复增加节点权重。

第一个 Master 节点(比如 k8s-master)中,创建 Keepalived 的配置文件,请自行更改 interface(网卡接口名称)与 virtual_ipaddress(虚拟 IP)配置项

1
2
# 创建配置文件,写入以下配置内容
# vim /usr/local/keepalived/etc/keepalived/keepalived.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
! Configuration File for keepalived

global_defs {
router_id k8s
}

vrrp_script check_haproxy {
script "/usr/local/keepalived/etc/keepalived/scripts/haproxy_check.sh"
interval 2
weight -100
fall 3
rise 2
}

vrrp_instance VI_1 {
state MASTER
interface enp0s3
virtual_router_id 51
priority 250
advert_int 1

authentication {
auth_type PASS
auth_pass ceb1b3ec013d66163d6ab
}

virtual_ipaddress {
192.168.2.100
}

track_script {
check_haproxy
}
}

第二个 Master 节点(比如 k8s-master2)中,创建 Keepalived 的配置文件,请自行更改 interface(网卡接口名称)与 virtual_ipaddress(虚拟 IP)配置项

1
2
# 创建配置文件,写入以下配置内容
# vim /usr/local/keepalived/etc/keepalived/keepalived.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
! Configuration File for keepalived

global_defs {
router_id k8s
}

vrrp_script check_haproxy {
script "/usr/local/keepalived/etc/keepalived/scripts/haproxy_check.sh"
interval 2
weight -100
fall 3
rise 2
}

vrrp_instance VI_1 {
state BACKUP
interface enp0s3
virtual_router_id 51
priority 200
advert_int 1

authentication {
auth_type PASS
auth_pass ceb1b3ec013d66163d6ab
}

virtual_ipaddress {
192.168.2.100
}

track_script {
check_haproxy
}
}

5. 启动服务

设置开机自启动 Keepalived 服务

1
2
3
4
5
# 更新系统配置
# systemctl daemon-reload

# 开机自启动Keepalived
# systemctl enable keepalived

立刻启动 Keepalived 服务

1
2
3
4
5
# 启动Keepalived
# systemctl start keepalived

# 查看Keepalived的运行状态
# systemctl status keepalived

当 Keepalived 的配置文件发生变更后,可以重启服务让其生效

1
2
# 重启Keepalived
# systemctl restart keepalived

6. 验证服务

通过 Keepalived 的 VIP(比如 192.168.2.100) + HaProxy 的 16443 反向代理端口(可自定义),验证 Keepalived 的 VIP 和 HaProxy 的反向代理功能是否可以正常工作,正常情况下 API Server 的健康检测接口会返回字符串 ok请自行更改 VIP 的地址

1
2
# 验证 Keepalived 的 VIP 与 HaProxy 的反向代理功能(注意,这里即使测试通过,只能说明网络连通性没问题,不能说明其他组件一定可以跟 API Server 通过 HTTPS 协议进行加密通信,尤其是涉及到证书问题时)
# curl --cacert /opt/kubernetes/ssl/ca.pem https://192.168.2.100:16443/healthz

7. 验证高可用性

验证目标

本节旨在验证 HaProxy + Keepalived 是否能够实现双机主备(即双机热备)高可用机制。具体而言,当第一个 Master 节点上的 HaProxy 发生宕机时,Keepalived 能够将虚拟 IP(VIP)自动切换到第二个 Master 节点上。当第一个 Master 节点上的 HaProxy 恢复运行时,虚拟 IP(VIP)会自动切换回第一个 Master 节点上。这样,在 Kubernetes 集群内部仍可通过 VIP 访问 API Server,从而保障 Kubernetes 集群整体服务的高可用性。

  • (1) 在第一个 Master 节点(比如 k8s-master)中,查看指定网卡(比如 enp0s3)的详细 IP 地址与状态信息,请自行更改网卡接口名称
1
2
# 查看网卡的详细信息
# ip a s enp0s3

输出示例如下,可以看到 VIP(虚拟 IP)地址 192.168.2.100 绑定了网卡接口 enp0s3

1
2
3
4
5
6
7
8
9
10
11
12
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 08:00:27:de:6f:6b brd ff:ff:ff:ff:ff:ff
inet 192.168.2.191/24 brd 192.168.2.255 scope global noprefixroute dynamic enp0s3
valid_lft 42617sec preferred_lft 42617sec
inet 192.168.2.100/32 scope global enp0s3
valid_lft forever preferred_lft forever
inet6 fe80::8b9e:77d3:4d79:eab9/64 scope link tentative noprefixroute dadfailed
valid_lft forever preferred_lft forever
inet6 fe80::6c1a:25c4:bdbd:dad3/64 scope link tentative noprefixroute dadfailed
valid_lft forever preferred_lft forever
inet6 fe80::8dee:f54a:fcd2:369b/64 scope link tentative noprefixroute dadfailed
valid_lft forever preferred_lft forever
  • (2) 在第一个 Master 节点(比如 k8s-master)中,关闭 HaProxy 服务,并查看 Keepalived 的运行状态
1
2
# 关闭Haproxy
# systemctl stop haproxy
1
2
# 查看Keepalived的运行状态
# systemctl status keepalived

输出示例如下,可以看到 Keepalived 扣减了当前节点的权重(计算公式:250 - 100 = 150),同时 VIP 也被移除了

1
2
3
4
5
6
7
8
11月 08 14:14:40 k8s-master Keepalived_vrrp[30483]: Sending gratuitous ARP on enp0s3 for 192.168.2.100
11月 08 14:14:40 k8s-master Keepalived_vrrp[30483]: Sending gratuitous ARP on enp0s3 for 192.168.2.100
11月 08 14:18:02 k8s-master Keepalived_vrrp[30483]: Script `check_haproxy` now returning 1
11月 08 14:18:06 k8s-master Keepalived_vrrp[30483]: VRRP_Script(check_haproxy) failed (exited with status 1)
11月 08 14:18:06 k8s-master Keepalived_vrrp[30483]: (VI_1) Changing effective priority from 250 to 150
11月 08 14:18:09 k8s-master Keepalived_vrrp[30483]: (VI_1) Master received advert from 192.168.2.148 with higher priority 200, ours 150
11月 08 14:18:09 k8s-master Keepalived_vrrp[30483]: (VI_1) Entering BACKUP STATE
11月 08 14:18:09 k8s-master Keepalived_vrrp[30483]: (VI_1) removing VIPs.
  • (3) 在第二个 Master 节点(比如 k8s-master2)中,查看指定网卡(比如 enp0s3)的详细 IP 地址与状态信息,请自行更改网卡接口名称
1
2
# 查看网卡的详细信息
# ip a s enp0s3

输出示例如下,可以看到 VIP(虚拟 IP)地址 192.168.2.100 绑定了网卡接口 enp0s3,也就是 VIP 成功切换到 BACKUP 节点

1
2
3
4
5
6
7
8
9
10
11
12
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 08:00:27:78:05:48 brd ff:ff:ff:ff:ff:ff
inet 192.168.2.148/24 brd 192.168.2.255 scope global noprefixroute dynamic enp0s3
valid_lft 41406sec preferred_lft 41406sec
inet 192.168.2.100/32 scope global enp0s3
valid_lft forever preferred_lft forever
inet6 fe80::8b9e:77d3:4d79:eab9/64 scope link tentative noprefixroute dadfailed
valid_lft forever preferred_lft forever
inet6 fe80::6c1a:25c4:bdbd:dad3/64 scope link tentative noprefixroute dadfailed
valid_lft forever preferred_lft forever
inet6 fe80::8dee:f54a:fcd2:369b/64 scope link tentative noprefixroute dadfailed
valid_lft forever preferred_lft forever
  • (4) 在第一个 Master 节点(比如 k8s-master)中,重新启动 HaProxy 服务,并查看指定网卡(比如 enp0s3)的详细 IP 地址与状态信息,请自行更改网卡接口名称
1
2
# 启动Haproxy
# systemctl start haproxy
1
2
# 查看网卡的详细信息
# ip a s enp0s3

输出示例如下,可以看到 VIP(虚拟 IP)地址 192.168.2.100 又绑定了网卡接口 enp0s3,也就是 VIP 再次切换回 MASTER 节点

1
2
3
4
5
6
7
8
9
10
11
12
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 08:00:27:de:6f:6b brd ff:ff:ff:ff:ff:ff
inet 192.168.2.191/24 brd 192.168.2.255 scope global noprefixroute dynamic enp0s3
valid_lft 42617sec preferred_lft 42617sec
inet 192.168.2.100/32 scope global enp0s3
valid_lft forever preferred_lft forever
inet6 fe80::8b9e:77d3:4d79:eab9/64 scope link tentative noprefixroute dadfailed
valid_lft forever preferred_lft forever
inet6 fe80::6c1a:25c4:bdbd:dad3/64 scope link tentative noprefixroute dadfailed
valid_lft forever preferred_lft forever
inet6 fe80::8dee:f54a:fcd2:369b/64 scope link tentative noprefixroute dadfailed
valid_lft forever preferred_lft forever

此时,若再次在第一个 Master 节点(比如 k8s-master)中查看 Keepalived 的运行状态,可以看到 Keepalived 将当前节点的权重恢复为初始值,输出示例如下

1
2
3
4
5
11月 08 14:23:49 k8s-master Keepalived_vrrp[30483]: VRRP_Script(check_haproxy) succeeded
11月 08 14:23:49 k8s-master Keepalived_vrrp[30483]: (VI_1) Changing effective priority from 150 to 250
11月 08 14:23:52 k8s-master Keepalived_vrrp[30483]: Sending gratuitous ARP on enp0s3 for 192.168.2.100
11月 08 14:23:52 k8s-master Keepalived_vrrp[30483]: Sending gratuitous ARP on enp0s3 for 192.168.2.100
11月 08 14:23:52 k8s-master Keepalived_vrrp[30483]: Sending gratuitous ARP on enp0s3 for 192.168.2.100
部署 Controller Manager

创建 Controller Manager 的配置文件,使用转义符 \\ 是为了使 EOF 保留换行符。一般情况下,以下配置内容可以直接拷贝使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 创建配置文件
# cat > /opt/kubernetes/cfg/kube-controller-manager.conf << EOF
KUBE_CONTROLLER_MANAGER_OPTS="--logtostderr=false \\
--v=2 \\
--log-dir=/opt/kubernetes/logs \\
--leader-elect=true \\
--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 \\
--secure-port=10257 \\
--kubeconfig=/opt/kubernetes/cfg/kube-controller-manager.kubeconfig \\
--authentication-kubeconfig=/opt/kubernetes/cfg/kube-controller-manager.kubeconfig \\
--authorization-kubeconfig=/opt/kubernetes/cfg/kube-controller-manager.kubeconfig \\
--client-ca-file=/opt/kubernetes/ssl/ca.pem \\
--use-service-account-credentials=true \\
--service-account-private-key-file=/opt/kubernetes/ssl/ca-key.pem \\
--cluster-signing-cert-file=/opt/kubernetes/ssl/ca.pem \\
--cluster-signing-key-file=/opt/kubernetes/ssl/ca-key.pem \\
--requestheader-client-ca-file=/opt/kubernetes/ssl/ca.pem \\
--tls-cert-file=/opt/kubernetes/ssl/kube-controller-manager.pem \\
--tls-private-key-file=/opt/kubernetes/ssl/kube-controller-manager-key.pem \\
--root-ca-file=/opt/kubernetes/ssl/ca.pem \\
--controllers=*,bootstrapsigner,tokencleaner \\
--deployment-controller-sync-period=10s \\
--enable-garbage-collector=true \\
--terminated-pod-gc-threshold=50 \\
--node-monitor-period=5s \\
--node-monitor-grace-period=20s \\
--pod-eviction-timeout=2m0s \\
--cluster-signing-duration=87600h0m0s"
EOF

配置参数说明

参数说明
--logtostderr是否将日志输出到标准错误,false 时输出到文件
--v日志级别(Verbose),数字越大日志越详细
--log-dir日志文件存放目录
--leader-elect启用 Leader 选举以保证高可用
--bind-addressController Manager 监听的本地 IP 地址
--allocate-node-cidrs自动为 Node 分配 Pod CIDR
--cluster-cidrPod 网络 CIDR,用于分配 Pod IP
--service-cluster-ip-rangeService 虚拟 IP 地址段
--secure-portController Manager HTTPS 端口
--kubeconfig用于访问 API Server 的 kubeconfig 文件
--authentication-kubeconfigController Manager 身份认证 kubeconfig
--authorization-kubeconfigController Manager 权限认证 kubeconfig
--client-ca-file验证客户端证书的 CA 文件
--use-service-account-credentials使用 ServiceAccount Token 调用 API Server
--service-account-private-key-file用于生成 ServiceAccount Token 的私钥
--cluster-signing-cert-file集群证书签名 CA 文件
--cluster-signing-key-file集群证书签名私钥
--requestheader-client-ca-file用于验证 API Server 聚合层(Aggregation Layer)客户端请求头证书的 CA 文件
--tls-cert-fileController Manager TLS 证书
--tls-private-key-fileController Manager TLS 私钥
--root-ca-file根 CA 文件,用于客户端和服务端通信验证
--controllers启用的 Controller 类型列表
--deployment-controller-sync-periodDeployment Controller 同步周期
--enable-garbage-collector启用垃圾回收机制,删除无用资源
--terminated-pod-gc-threshold触发终止 Pod 垃圾回收的阈值
--node-monitor-period节点状态检测周期
--node-monitor-grace-period节点不可达后等待时间
--pod-eviction-timeoutPod 被逐出前等待的超时时间
--cluster-signing-duration集群签发证书的有效期(示例:87600h0m0s 表示 10 年)

生成 kube-controller-manager.kubeconfig 配置文件,请自行修改这里环境变量中的 API Server 地址,且指定为 Keepalived 的虚拟 IP 地址(比如 192.168.2.100) + HaProxy 反向代理的端口(比如 16443),切勿直接拷贝环境变量的值

1
2
# 临时添加环境变量
KUBE_APISERVER="https://192.168.2.100:16443" # API Server 的地址,搭建 K8s 高可用集群时,需要使用 Keepalived 的虚拟 IP 地址 + HaProxy 反向代理的端口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 设置集群信息
# kubectl config set-cluster kubernetes \
--certificate-authority=/opt/kubernetes/ssl/ca.pem \
--embed-certs=true \
--server=${KUBE_APISERVER} \
--kubeconfig=kube-controller-manager.kubeconfig

# 设置客户端认证信息
# kubectl config set-credentials system:kube-controller-manager \
--client-certificate=/opt/kubernetes/ssl/kube-controller-manager.pem \
--client-key=/opt/kubernetes/ssl/kube-controller-manager-key.pem \
--embed-certs=true \
--kubeconfig=kube-controller-manager.kubeconfig

# 设置上下文
# kubectl config set-context system:kube-controller-manager@kubernetes \
--cluster=kubernetes \
--user=system:kube-controller-manager \
--kubeconfig=kube-controller-manager.kubeconfig

# 切换上下文
# kubectl config use-context system:kube-controller-manager@kubernetes \
--kubeconfig=kube-controller-manager.kubeconfig
1
2
# 移动生成的配置文件
# mv kube-controller-manager.kubeconfig /opt/kubernetes/cfg

配置 Systemd 管理 Controller Manager 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建 Controller Manager 服务管理的配置文件
# cat > /usr/lib/systemd/system/kube-controller-manager.service << EOF
[Unit]
Description=Kubernetes Controller Manager
Documentation=https://github.com/kubernetes/kubernetes
After=network-online.target

[Service]
EnvironmentFile=/opt/kubernetes/cfg/kube-controller-manager.conf
ExecStart=/opt/kubernetes/bin/kube-controller-manager \$KUBE_CONTROLLER_MANAGER_OPTS
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF

启动并设置开机自启动 Controller Manager 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 更新系统配置
# systemctl daemon-reload

# 开机自启动 Controller Manager 服务
# systemctl enable kube-controller-manager

# 启动 Controller Manager 服务
# systemctl start kube-controller-manager

# 查看 Controller Manager 服务的运行状态
# systemctl status kube-controller-manager

# 查看 Controller Manager 服务的启动日志(可用于排查启动问题)
# journalctl -u kube-controller-manager.service

当 Controller Manager 启动后,在查看运行状态时,可能会看到以下警告 / 错误信息。如果 K8s 集群是裸机或本地环境(没有 AWS / Azure 等云提供商),这两个报错可以忽略,它们不会影响 K8s 集群的核心功能

1
2
11月 11 18:58:54 k8s-master1 kube-controller-manager[15484]: E1111 18:58:54.496737   15484 core.go:90] Failed to start service controller: WARNING: no cloud provider provided, services of type LoadBalancer will fail
11月 11 18:59:07 k8s-master1 kube-controller-manager[15484]: E1111 18:59:07.629134 15484 core.go:230] failed to start cloud node lifecycle controller: no cloud provider provided
部署 Scheduler

创建 Scheduler 配置文件,使用转义符 \\ 是为了使 EOF 保留换行符。一般情况下,以下配置内容可以直接拷贝使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 创建配置文件
# cat > /opt/kubernetes/cfg/kube-scheduler.conf << EOF
KUBE_SCHEDULER_OPTS="--logtostderr=false \\
--v=2 \\
--log-dir=/opt/kubernetes/logs \\
--leader-elect=true \\
--bind-address=127.0.0.1 \\
--secure-port=10259 \\
--kubeconfig=/opt/kubernetes/cfg/kube-scheduler.kubeconfig \\
--authentication-kubeconfig=/opt/kubernetes/cfg/kube-scheduler.kubeconfig \\
--authorization-kubeconfig=/opt/kubernetes/cfg/kube-scheduler.kubeconfig \\
--client-ca-file=/opt/kubernetes/ssl/ca.pem \\
--requestheader-client-ca-file=/opt/kubernetes/ssl/ca.pem \\
--tls-cert-file=/opt/kubernetes/ssl/kube-scheduler.pem \\
--tls-private-key-file=/opt/kubernetes/ssl/kube-scheduler-key.pem"
EOF

配置参数说明

参数说明
--logtostderr是否将日志输出到标准错误(false 表示输出到文件,需要配合 --log-dir 使用)。
--v日志级别,数值越大日志越详细。一般设置为 2
--log-dir日志文件输出目录(当 logtostderr=false 时有效)。
--leader-elect启用 Leader 选举,用于多个 Scheduler 实例保证高可用。
--bind-addressScheduler 监听地址,127.0.0.1 表示仅允许本机访问。
--secure-portScheduler HTTPS 服务监听端口,默认是 10259
--kubeconfigScheduler 访问 API Server 的凭证文件。
--authentication-kubeconfigAPI Server 调用 Scheduler 的认证配置。
--authorization-kubeconfigAPI Server 调用 Scheduler 的授权配置。
--client-ca-file用于验证 API Server 证书的 CA 文件。
--requestheader-client-ca-file用于验证 API Server 聚合层(Aggregation Layer)客户端请求头证书的 CA 文件。
--tls-cert-fileScheduler 作为 HTTPS 服务端的证书。
--tls-private-key-fileScheduler 服务证书对应的私钥。

生成 kube-scheduler.kubeconfig 配置文件,请自行修改这里环境变量中的 API Server 地址,且指定为 Keepalived 的虚拟 IP 地址(比如 192.168.2.100) + HaProxy 反向代理的端口(比如 16443),切勿直接拷贝环境变量的值

1
2
# 临时添加环境变量
KUBE_APISERVER="https://192.168.2.100:16443" # API Server 的地址,搭建 K8s 高可用集群时,需要使用 Keepalived 的虚拟 IP 地址 + HaProxy 反向代理的端口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 设置集群信息
# kubectl config set-cluster kubernetes \
--certificate-authority=/opt/kubernetes/ssl/ca.pem \
--embed-certs=true \
--server=${KUBE_APISERVER} \
--kubeconfig=kube-scheduler.kubeconfig

# 设置客户端认证信息
# kubectl config set-credentials system:kube-scheduler \
--client-certificate=/opt/kubernetes/ssl/kube-scheduler.pem \
--client-key=/opt/kubernetes/ssl/kube-scheduler-key.pem \
--embed-certs=true \
--kubeconfig=kube-scheduler.kubeconfig

# 设置上下文
# kubectl config set-context system:kube-scheduler@kubernetes \
--cluster=kubernetes \
--user=system:kube-scheduler \
--kubeconfig=kube-scheduler.kubeconfig

# 切换上下文
# kubectl config use-context system:kube-scheduler@kubernetes \
--kubeconfig=kube-scheduler.kubeconfig
1
2
# 移动生成的配置文件
# mv kube-scheduler.kubeconfig /opt/kubernetes/cfg

配置 Systemd 管理 Scheduler 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建 Scheduler 服务管理的配置文件
# cat > /usr/lib/systemd/system/kube-scheduler.service << EOF
[Unit]
Description=Kubernetes Scheduler
Documentation=https://github.com/kubernetes/kubernetes
After=network-online.target

[Service]
EnvironmentFile=/opt/kubernetes/cfg/kube-scheduler.conf
ExecStart=/opt/kubernetes/bin/kube-scheduler \$KUBE_SCHEDULER_OPTS
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF

启动并设置开机自启动 Scheduler 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 更新系统配置
# systemctl daemon-reload

# 开机自启动 Scheduler 服务
# systemctl enable kube-scheduler

# 启动 Scheduler 服务
# systemctl start kube-scheduler

# 查看 Scheduler 服务的运行状态
# systemctl status kube-scheduler

# 查看 Scheduler 服务的启动日志(可用于排查启动问题)
# journalctl -u kube-scheduler.service

当 API Server、Controller Manager、Scheduler 组件都成功启动后,可以通过 kubectl 工具查看当前 Kubernetes 集群组件(不包括 API Server)的运行状态

1
2
3
4
5
6
7
8
# 查看核心组件的运行状态(不包括 API Server)
# kubectl get cs
NAME STATUS MESSAGE ERROR
scheduler Healthy ok
controller-manager Healthy ok
etcd-0 Healthy {"health":"true"}
etcd-1 Healthy {"health":"true"}
etcd-2 Healthy {"health":"true"}

提示

这里的 kubectl get cs 命令只会显示部分组件的运行状态(主要是 Etcd、Controller Manager、Scheduler),不包括 API Server;因为 API Server 是 Kubernetes 的核心服务,一般需要通过其它方式进行监控。

部署 Master 与 Node 节点

特别注意

  • 在 Kubernetes 集群的所有节点(包括 Master 和 Node 节点)上分别执行以下操作,除了特别说明之外(比如,只在 Master 节点批准 Kubelet 的证书签名请求)。简而言之,Master 和 Node 节点都需要安装 Kubelet、Kube-Proxy、CNI 网络插件。
  • 如果 Master 节点仅用于运行控制平面组件(如 API Server、Controller Manager 和 Scheduler),不承载用户 Pod 的调度和运行任务,则 Master 节点可以不部署 Kubelet 和 Kube-Proxy,但还是必须安装 CNI 网络插件。。
下载二进制文件

下载地址

  • Kubernetes 相关的二进制包可以从 GitHub 下载得到,这里下载的版本是 v1.19.10
  • 当打开 GitHub 链接后,会发现里面有很多二进制包,只需要下载一个 Server 二进制包就够了,里面包含了 Master 节点相关的二进制包。

由于上面单独部署 Maser 节点时,已经下载并解压过 Kubernetes 的二进制包文件,因此这里只需要在所有 Node 节点上下载二进制包文件即可

1
2
3
4
5
# 下载二进制包(耗时较长)
# wget https://dl.k8s.io/v1.19.10/kubernetes-server-linux-amd64.tar.gz

# 解压文件
# tar zxvf kubernetes-server-linux-amd64.tar.gz

在所有节点(包括 Master 和 Node 节点)上,分别执行以下命令拷贝对应的可执行文件

1
2
3
4
5
6
7
8
9
10
11
# 进入解压目录
# cd kubernetes/server/bin

# 创建目录
# mkdir -p /opt/kubernetes/{bin,cfg,ssl,logs}

# 拷贝文件
# cp kubelet kube-proxy /opt/kubernetes/bin

# 拷贝文件
# cp -n kubectl /usr/bin/
部署 Kubelet

创建 Kubelet 的配置文件,使用转义符 \\ 是为了使 EOF 保留换行符,请自行更改 --hostname-override 的值为当前节点的主机名(比如 k8s-node1),切勿直接拷贝以下配置内容

1
2
3
4
5
6
7
8
9
10
11
12
13
# 创建配置文件
# cat > /opt/kubernetes/cfg/kubelet.conf << EOF
KUBELET_OPTS="--logtostderr=false \\
--v=2 \\
--log-dir=/opt/kubernetes/logs \\
--hostname-override=<当前节点的主机名> \\
--network-plugin=cni \\
--kubeconfig=/opt/kubernetes/cfg/kubelet.kubeconfig \\
--bootstrap-kubeconfig=/opt/kubernetes/cfg/bootstrap.kubeconfig \\
--config=/opt/kubernetes/cfg/kubelet-config.yml \\
--cert-dir=/opt/kubernetes/ssl \\
--pod-infra-container-image=lizhenliang/pause-amd64:3.0"
EOF

配置参数说明

参数说明
--logtostderr是否将日志输出到标准错误(false 表示写入日志目录)
--v日志详细级别,值越大日志越详细(2 为常用调试级别)
--log-dir日志存放目录
--hostname-override覆盖节点的主机名,在集群中显示为指定名称,通常填写当前节点的主机名
--network-plugin使用的网络插件类型(比如 CNI)
--kubeconfigkubelet 连接 API Server 的 kubeconfig 配置文件路径
--bootstrap-kubeconfigkubelet 首次启动时用于申请证书的引导配置文件路径
--configkubelet 主配置文件路径
--cert-dir存放 kubelet TLS 证书的目录
--pod-infra-container-imagePod 基础容器(Pause 容器)镜像,用于提供网络命名空间

创建 Kubelet 的配置参数文件,一般情况下,以下配置内容可以直接拷贝使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 创建配置参数文件
# cat > /opt/kubernetes/cfg/kubelet-config.yml << EOF
kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
address: 0.0.0.0
port: 10250
readOnlyPort: 10255
cgroupDriver: cgroupfs
clusterDNS:
- 10.0.0.2
clusterDomain: cluster.local
failSwapOn: false
authentication:
anonymous:
enabled: false
webhook:
cacheTTL: 2m0s
enabled: true
x509:
clientCAFile: /opt/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: 1000000
maxPods: 110
EOF

配置参数说明

参数说明备注
kind资源类型固定为 KubeletConfiguration
apiVersion配置 API 版本当前常用版本为 kubelet.config.k8s.io/v1beta1
addressKubelet 监听的 IP 地址0.0.0.0 表示监听所有网卡地址
portKubelet 的主要服务端口默认值为 10250,用于 API Server 与 Kubelet 通信(带认证)
readOnlyPort只读端口默认值为 10255(不建议对外开放),提供只读状态信息
cgroupDrivercgroup 驱动类型与容器运行时(如 Docker、Containerd)保持一致,常见值:systemdcgroupfs
clusterDNS集群内 DNS 服务 IP 地址通常设置为 Kube-DNS 或 CoreDNS 的 ClusterIP,如 10.0.0.2
clusterDomain集群内部 DNS 域名后缀默认值为 cluster.local
failSwapOn是否在系统启用 Swap 分区时拒绝启动建议 false(否则 Swap 分区未关闭时 Kubelet 会报错)
authentication.anonymous.enabled是否允许匿名访问建议设为 false,禁止匿名访问,提高安全性
authentication.webhook.enabled是否启用 Webhook 认证启用后可通过 API Server 的 Token 校验机制认证
authentication.webhook.cacheTTLWebhook 认证缓存时间2m0s 表示缓存 2 分钟
authentication.x509.clientCAFile客户端证书 CA 文件路径指向集群 CA 证书文件
authorization.mode授权模式常用值为 Webhook,即由 API Server 进行鉴权
authorization.webhook.cacheAuthorizedTTL授权缓存时间(通过)配置示例:5m0s
authorization.webhook.cacheUnauthorizedTTL授权缓存时间(未通过)配置示例:30s
evictionHard.imagefs.available镜像文件系统最小可用空间阈值低于该值会触发 Pod 驱逐,例如 15%
evictionHard.memory.available节点可用内存阈值低于 100Mi 时触发 Pod 驱逐
evictionHard.nodefs.available节点文件系统最小可用空间阈值低于 10% 时触发 Pod 驱逐
evictionHard.nodefs.inodesFree节点可用 inode 阈值低于 5% 时触发 Pod 驱逐
maxOpenFilesKubelet 允许打开的最大文件数默认值较小,建议设置为 1000000
maxPods每个节点允许运行的最大 Pod 数默认值为 110,可根据节点的硬件性能调整

生成 bootstrap.kubeconfig 配置文件,请自行修改这里环境变量中的 API Server 地址,且指定为 Keepalived 的虚拟 IP 地址(比如 192.168.2.100) + HaProxy 反向代理的端口(比如 16443),切勿直接拷贝环境变量的值

1
2
3
# 临时添加环境变量
KUBE_APISERVER="https://192.168.2.100:16443" # API Server 的地址,搭建 K8s 高可用集群时,需要使用 Keepalived 的虚拟 IP 地址 + HaProxy 反向代理的端口
TOKEN="c47ffb939f5ca36231d9e3121a252940" # 必须与 token.csv 文件里的 token 保持一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 设置集群信息
# kubectl config set-cluster kubernetes \
--certificate-authority=/opt/kubernetes/ssl/ca.pem \
--embed-certs=true \
--server=${KUBE_APISERVER} \
--kubeconfig=bootstrap.kubeconfig

# 设置客户端认证信息
# kubectl config set-credentials "kubelet-bootstrap" \
--token=${TOKEN} \
--kubeconfig=bootstrap.kubeconfig

# 设置上下文
# kubectl config set-context default \
--cluster=kubernetes \
--user="kubelet-bootstrap" \
--kubeconfig=bootstrap.kubeconfig

# 切换上下文
# kubectl config use-context default --kubeconfig=bootstrap.kubeconfig
1
2
# 移动生成的配置文件
# mv bootstrap.kubeconfig /opt/kubernetes/cfg

提前手动拉取 Pause 镜像(Pod 的基础镜像,用于 Pod 网络命名空间),避免后面部署 CNI 网络插件时,Kubelet 出现自动拉取 Pause 镜像失败的问题(该镜像的名称和版本在 kubelet.conf 配置文件中通过 --pod-infra-container-image 指定)

1
2
# 提前拉取Pause镜像(可选操作)
# docker pull lizhenliang/pause-amd64:3.0

配置 Systemd 管理 Kubelet 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建 Kubelet 服务管理的配置文件
# cat > /usr/lib/systemd/system/kubelet.service << EOF
[Unit]
Description=Kubernetes Kubelet
Documentation=https://github.com/kubernetes/kubernetes
After=network-online.target
After=docker.service

[Service]
EnvironmentFile=/opt/kubernetes/cfg/kubelet.conf
ExecStart=/opt/kubernetes/bin/kubelet \$KUBELET_OPTS
Restart=on-failure
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF

启动并设置开机自启动 Kubelet 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 更新系统配置
# systemctl daemon-reload

# 开机自启动 Kubelet 服务
# systemctl enable kubelet

# 启动 Kubelet 服务
# systemctl start kubelet

# 查看 Kubelet 服务的运行状态(若有网络错误信息,可暂时忽略)
# systemctl status kubelet

# 查看 Kubelet 服务的启动日志(可用于排查启动问题)
# journalctl -u kubelet.service

在查看 Kubelet 服务的运行状态时,可能会看到以下错误信息。这是因为目前还没有安装 CNI 网络插件,因此该错误可以暂时忽略掉

1
2
3
11月 11 21:15:55 k8s-master1 kubelet[7509]: E1112 11:15:55.312188    7509 kubelet.go:2134] Container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized
11月 11 21:16:00 k8s-master1 kubelet[7509]: E1112 11:16:00.329434 7509 kubelet.go:2134] Container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized
11月 11 21:16:05 k8s-master1 kubelet[7509]: E1112 11:16:05.347068 7509 kubelet.go:2134] Container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized

特别注意

当 Kubelet 服务正常启动后,会自动向 API Server 发起 CSR(证书签名请求)。一旦该 CSR 请求被批准,Kubelet 就会获得客户端证书,并开始与 API Server 通信,从而将它所在的 Node 节点加入到 Kubernetes 集群中,所以无需使用 kubeadm join 命令手动将当前节点加入 Kubernetes 集群,也无需像 Kube-Proxy 一样使用自签 CA 签发证书。

在任意一个 Master 节点上执行以下命令,批准 Kubelet 证书签名请求并允许 Node 节点加入 Kubernetes 集群

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 查看 Kubernetes 集群中的所有证书签名请求
# kubectl get csr
NAME AGE SIGNERNAME REQUESTOR CONDITION
node-csr-B91KCv7T_4A4RCfvfEBfFOP3V5zCYKK5NQkVLDmyly4 86s kubernetes.io/kube-apiserver-client-kubelet kubelet-bootstrap Pending
node-csr-F4zdBAJxxKnDLsWJbYHVi_XKF8Uwh1MMarXNJiHAUy0 85s kubernetes.io/kube-apiserver-client-kubelet kubelet-bootstrap Pending
node-csr-GYbsPLYySA0LiUkuhbpXlApj-JC-TDnA7SVRPHWU2mI 88s kubernetes.io/kube-apiserver-client-kubelet kubelet-bootstrap Pending
node-csr-bLUS_w2or-Rq9c5JgCzaf5e6QYHND6PTEqQFTvDsY5g 96s kubernetes.io/kube-apiserver-client-kubelet kubelet-bootstrap Pending
node-csr-ndBBynU94khVf6ADv4pdmtrlsxDOXAnyxu18e_VnhBU 87s kubernetes.io/kube-apiserver-client-kubelet kubelet-bootstrap Pending

# 批准证书签名请求(请根据上面输出的证书签名请求名称,自行更改命令行参数)
# kubectl certificate approve node-csr-B91KCv7T_4A4RCfvfEBfFOP3V5zCYKK5NQkVLDmyly4
# kubectl certificate approve node-csr-F4zdBAJxxKnDLsWJbYHVi_XKF8Uwh1MMarXNJiHAUy0
# kubectl certificate approve node-csr-GYbsPLYySA0LiUkuhbpXlApj-JC-TDnA7SVRPHWU2mI
# kubectl certificate approve node-csr-bLUS_w2or-Rq9c5JgCzaf5e6QYHND6PTEqQFTvDsY5g
# kubectl certificate approve node-csr-ndBBynU94khVf6ADv4pdmtrlsxDOXAnyxu18e_VnhBU

# 查看集群中所有节点的状态(由于 CNI 网络插件还没有部署,因此节点会处于没有准备就绪状态 - NotReady)
# kubectl get node
NAME STATUS ROLES AGE VERSION
k8s-master1 NotReady <none> 10m v1.19.10
k8s-master2 NotReady <none> 18s v1.19.10
k8s-node1 NotReady <none> 29s v1.19.10
k8s-node2 NotReady <none> 22s v1.19.10
k8s-node3 NotReady <none> 25s v1.19.10
部署 Kube-Proxy

创建 Kube-Proxy 的配置文件,使用转义符 \\ 是为了使 EOF 保留换行符,以下配置内容可以直接拷贝使用

1
2
3
4
5
6
# cat > /opt/kubernetes/cfg/kube-proxy.conf << EOF
KUBE_PROXY_OPTS="--logtostderr=false \\
--v=2 \\
--log-dir=/opt/kubernetes/logs \\
--config=/opt/kubernetes/cfg/kube-proxy-config.yml"
EOF

配置参数说明

参数说明
--logtostderr=false日志输出到文件而不是标准错误输出(stderr
--v=2设置日志详细级别为 2(数值越大日志越详细)
--log-dir=/opt/kubernetes/logs指定日志文件存放目录
--config=/opt/kubernetes/cfg/kube-proxy-config.yml指定 kube-proxy 的配置参数文件路径

创建 Kube-Proxy 的配置参数文件,请自行更改 hostnameOverride 的值为当前节点的主机名(比如 k8s-node1),其中的 clusterCIDR 要与 kube-apiserver.conf 配置文件中的 --service-cluster-ip-range 一致,切勿直接拷贝以下配置内容

1
2
3
4
5
6
7
8
9
10
11
12
# 创建配置参数文件
# cat > /opt/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: /opt/kubernetes/cfg/kube-proxy.kubeconfig
hostnameOverride: <当前节点的主机名>
clusterCIDR: 10.0.0.0/24
mode: "iptables"
EOF

配置参数说明

参数说明备注
kind资源类型,固定为 KubeProxyConfiguration必填
apiVersion配置文件的 API 版本,当前主流为 kubeproxy.config.k8s.io/v1alpha1必填
bindAddressKube-Proxy 监听的地址,用于服务代理流量一般使用 0.0.0.0 监听所有网卡
metricsBindAddress暴露 Prometheus 指标的监听地址默认端口 10249,可用于监控
clientConnection.kubeconfigKube-Proxy 访问 API Server 的 kubeconfig 文件路径必填,包含 system:kube-proxy 证书信息
hostnameOverride当前节点的主机名,注册到集群时的 Node 名称建议与系统主机名一致
clusterCIDRPod 网段,需与 Controller Manager 的 --cluster-cidr 一致必填,否则可能导致 Service 转发异常
modeKube-Proxy 的工作模式,可选值 iptablesipvs默认 iptables,兼容性最好;ipvs 性能更好,但要求节点的操作系统内核支持 ip_vs 模块

生成 kube-proxy.kubeconfig 文件,请自行修改这里环境变量中的 API Server 地址,且指定为 Keepalived 的虚拟 IP 地址(比如 192.168.2.100) + HaProxy 反向代理的端口(比如 16443),切勿直接拷贝环境变量的值

1
2
# 临时添加环境变量
KUBE_APISERVER="https://192.168.2.100:16443" # API Server 的地址,搭建 K8s 高可用集群时,需要使用 Keepalived 的虚拟 IP 地址 + HaProxy 反向代理的端口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 设置集群信息
# kubectl config set-cluster kubernetes \
--certificate-authority=/opt/kubernetes/ssl/ca.pem \
--embed-certs=true \
--server=${KUBE_APISERVER} \
--kubeconfig=kube-proxy.kubeconfig

# 设置客户端认证信息
# kubectl config set-credentials kube-proxy \
--client-certificate=/opt/kubernetes/ssl/kube-proxy.pem \
--client-key=/opt/kubernetes/ssl/kube-proxy-key.pem \
--embed-certs=true \
--kubeconfig=kube-proxy.kubeconfig

# 设置上下文
# kubectl config set-context default \
--cluster=kubernetes \
--user=kube-proxy \
--kubeconfig=kube-proxy.kubeconfig

# 切换上下文
# kubectl config use-context default --kubeconfig=kube-proxy.kubeconfig
1
2
# 移动生成的配置文件
# mv kube-proxy.kubeconfig /opt/kubernetes/cfg

配置 Systemd 管理 Kube-Proxy 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建 Kube-Proxy 服务管理的配置文件
# cat > /usr/lib/systemd/system/kube-proxy.service << EOF
[Unit]
Description=Kubernetes Proxy
Documentation=https://github.com/kubernetes/kubernetes
After=network-online.target
After=docker.service

[Service]
EnvironmentFile=/opt/kubernetes/cfg/kube-proxy.conf
ExecStart=/opt/kubernetes/bin/kube-proxy \$KUBE_PROXY_OPTS
Restart=on-failure
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF

启动并设置开机自启动 Kube-Proxy 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 更新系统配置
# systemctl daemon-reload

# 开机自启动 Kube-Proxy 服务
# systemctl enable kube-proxy

# 启动 Kube-Proxy 服务
# systemctl start kube-proxy

# 查看 Kube-Proxy 服务的运行状态
# systemctl status kube-proxy

# 查看 Kube-Proxy 服务的启动日志(可用于排查启动问题)
# journalctl -u kube-proxy.service
部署 CNI 网络插件
所有节点的操作

特别注意

在 Kubernetes 集群的所有节点(包括 Master 和 Node 节点)上面,分别下载并解压 CNI 网络插件的二进制包

1
2
3
4
5
6
7
8
9
# 下载二进制文件
# wget https://github.com/containernetworking/plugins/releases/download/v0.8.6/cni-plugins-linux-amd64-v0.8.6.tgz

# 创建目录
# mkdir -p /opt/cni/bin
# mkdir -p /etc/cni/net.d/

# 解压文件
# tar zxvf cni-plugins-linux-amd64-v0.8.6.tgz -C /opt/cni/bin
Master 节点的操作

特别注意

在任意一个 Master 节点上面部署 CNI 网络插件(Flannel),不需要在所有的 Master 节点和 Node 节点中重复执行。简而言之,以下操作仅需要在任意一个 Master 节点上执行

下载 CNI 网络插件(Flannel)的 YAML 配置文件

1
2
# 下载配置文件
# wget -P /opt/kubernetes/cfg/ https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

在 Kubernetes 集群中部署 CNI 网络插件(Flannel)

1
2
# 部署 CNI 网络插件(Flannel),成功后所有节点会自动下载相应的 Docker 镜像(如果本地不存在)
# kubectl apply -f /opt/kubernetes/cfg/kube-flannel.yml

查看 CNI 网络插件的所有 Pod

1
2
# 查看所有 Pod
# kubectl get pods -n kube-flannel
1
2
3
4
5
6
NAME                        READY   STATUS    RESTARTS   AGE
pod/kube-flannel-ds-29gvn 1/1 Running 0 5m14s
pod/kube-flannel-ds-99gvw 1/1 Running 0 5m14s
pod/kube-flannel-ds-ntsf5 1/1 Running 0 5m14s
pod/kube-flannel-ds-xqrnd 1/1 Running 0 5m14s
pod/kube-flannel-ds-xwr2h 1/1 Running 0 5m14s

查看 CNI 网络插件的所有 DaemonSet

1
2
# 查看所有 DaemonSet
# kubectl get ds -n kube-flannel
1
2
NAME              DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
kube-flannel-ds 5 5 5 5 5 <none> 6m59s

查看 Kubernetes 集群中所有节点的运行状态

1
2
# 查看集群中所有节点的运行状态(当 CNI 网络插件部署完成后,所有节点都会自动切换到准备就绪状态 - Ready)
# kubectl get node
1
2
3
4
5
6
NAME          STATUS   ROLES    AGE     VERSION
k8s-master1 Ready <none> 5h28m v1.19.10
k8s-master2 Ready <none> 5h17m v1.19.10
k8s-node1 Ready <none> 5h17m v1.19.10
k8s-node2 Ready <none> 5h17m v1.19.10
k8s-node3 Ready <none> 5h17m v1.19.10

CNI 网络插件的安装细节

这个 kubectl apply 命令会创建一个 DaemonSet 类型的资源,而 DaemonSet 的特性是:会在 Kubernetes 集群中所有「可调度」的 Node 节点上运行一个 Pod 副本。换言之,在任意一个 Master 节点上,通过 kubectl apply 命令安装 CNI 网络插件后,默认会将 CNI 网络插件部署到整个 Kubernetes 集群的所有节点(包括 Master 和 Node),无需手动在每个节点上重复执行 kubectl apply 命令来安装 CNI 网络插件。

创建用于授权 API Server 访问 Kubelet 的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 创建配置文件
# cat > /opt/kubernetes/cfg/apiserver-to-kubelet-rbac.yaml << EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
annotations:
rbac.authorization.kubernetes.io/autoupdate: "true"
labels:
kubernetes.io/bootstrapping: rbac-defaults
name: system:kube-apiserver-to-kubelet
rules:
- apiGroups:
- ""
resources:
- nodes/proxy
- nodes/stats
- nodes/log
- nodes/spec
- nodes/metrics
- pods/log
verbs:
- "*"

---

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: system:kube-apiserver
namespace: ""
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:kube-apiserver-to-kubelet
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: kubernetes
EOF

授权 API Server 访问 Kubelet

1
2
# 授权访问
# kubectl apply -f /opt/kubernetes/cfg/apiserver-to-kubelet-rbac.yaml

为什么必须安装 CNI 网络插件

  • Kubernetes 本身不包含网络实现,但它要求集群中的所有 Pod 能够彼此通信(无论位于哪个节点),这是 Kubernetes 网络模型的基本要求。
  • 通过 Kubeadm 搭建 Kubernetes 集群时,使用 kubeadm init 初始化 Master 节点后,默认只有控制平面功能,没有网络功能。
  • 当没有安装 CNI 网络插件时,Pod 会卡在 ContainerCreating 状态,因为找不到网络(CNI)配置。
  • 当安装 CNI 网络插件(如 Flannel)后,多个节点之间的 Pod 才能通信和正常运行。

测试集群整体功能

在任意一个 Master 节点中执行以下命令,查看集群中所有节点的运行状态,当节点的运行状态都变更为 Ready 时,则表示 Kubernetes 集群已经成功搭建起来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 查看所有节点的运行状态
# kubectl get nodes
NAME STATUS ROLES AGE VERSION
k8s-master1 Ready <none> 5h32m v1.19.10
k8s-master2 Ready <none> 5h21m v1.19.10
k8s-node1 Ready <none> 5h22m v1.19.10
k8s-node2 Ready <none> 5h22m v1.19.10
k8s-node3 Ready <none> 5h22m v1.19.10

# 查看核心组件的运行状态(不包括 API Server)
# kubectl get cs
NAME STATUS MESSAGE ERROR
controller-manager Healthy ok
scheduler Healthy ok
etcd-2 Healthy {"health":"true"}
etcd-0 Healthy {"health":"true"}
etcd-1 Healthy {"health":"true"}

# 查看集群版本
# kubectl version --short
Client Version: v1.19.10
Server Version: v1.19.10

特别注意

在使用二进制方式完成 Kubernetes 集群的搭建时,执行 kubectl get pods -n kube-system 命令可能无法看到任何 Pod 的运行状态。这是因为 Kubernetes 中的 Pod 是由 Kubelet 负责向 API Server 注册并创建的,仅仅启动 API Server、Controller Manager、Scheduler、Kubelet、Kube-Proxy 等二进制组件,并不会自动生成或运行这些系统组件对应的 Pod。系统核心组件如 CoreDNS、Metrics Server 等都需要用户手动部署相关的 YAML 资源文件,Kubelet 才会拉取镜像并创建对应的 Pod。因此,若未部署这些资源,kube-system 命名空间中将不会显示任何 Pod。

在任意一个 Master 节点中执行以下命令,目的是在 Kubernetes 集群里创建一个 Nginx 的 Deployment,验证 Kubernetes 集群是否正常运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 创建Nginx
# kubectl create deployment nginx --image=nginx

# 暴露Nginx的端口
# kubectl expose deployment nginx --port=80 --type=NodePort --target-port=80

# 查看Pod列表
# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-6799fc88d8-w8g9b 1/1 Running 0 64s 10.244.2.2 k8s-node3 <none> <none>

# 查看Service列表
# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.0.0.1 <none> 443/TCP 6h42m
nginx NodePort 10.0.0.38 <none> 80:31603/TCP 64s

在 Kubernetes 集群外部,通过浏览器访问 http://192.168.2.191:31603,其中 IP 可以是任意集群节点的 IP 地址,端口由 kubectl get svc 命令可得知。若 Ngninx 容器在 Kubernetes 集群中创建并启动成功,则浏览器可以正常访问 Nginx 的首页(如下图所示),如下图所示:

若希望删除刚在 Kubernetes 集群创建的 Nginx 容器,可以在任意一个 Master 节点上执行以下命令:

1
2
3
4
5
# 删除Service
# kubectl delete service nginx

# 删除Deployment
# kubectl delete deployment nginx

部署 CoreDNS(可选)

特别注意

  • 以下所有操作仅在任意一个 Master 节点上执行,不需要在所有的 Master 节点和 Node 节点中重复执行,请保证在部署 CoreDNS 之前,Kubernetes 集群是可以正常运行的。
  • CodreDNS 的作用是:在 Kubernetes 集群内,可以通过 Service 的 DNS 名称(域名)对 Pod 进行访问,比如:http://<service-name>.<namespace>.svc.cluster.local:<port>,具体使用案例请看 这里

CoreDNS 的部署说明

  • 由于上面搭建的 Kubernetes 集群已经具备运行 Pod 的完整容器化环境,这种情况下 CoreDNS 最合适的部署方式就是容器化部署,即通过 Kubernetes 自己来管理 CoreDNS 的 Pod。CoreDNS 也可以通过二进制包进行部署的,两种部署方式的区别如下:
部署方式适合场景优点缺点
容器化部署 CoreDNS 集群支持运行 Pod,且至少 kube-system 命名空间可用符合 Kubernetes 生态,自动扩缩容、自动重启,便于管理需要容器运行环境
二进制部署 CoreDNS 极简集群、节点无容器环境,或仅测试 DNS 不依赖容器,部署简单无法通过 Kubernetes 管理,需手动维护、高可用难度大
  • 容器化部署 CoreDNS 的几个关键优势如下:
优势说明
符合 Kubernetes 官方架构 CoreDNS 本身就是官方默认的集群 DNS 方案,部署成 Pod 完全符合设计思路。
自动高可用可通过 Replica SetDeployment 管理多个副本,某个 Pod 异常退出会被自动重建。
易扩容 / 升级直接通过 kubectl scalekubectl apply 即可扩容、升级。
与 kube-proxy、kubelet 无缝配合 kubelet 会自动配置 Pod 的 DNS 指向 CoreDNS 的 ClusterIP。
支持弹性伸缩可配合 Pod 横向自动扩容(Horizontal Pod Autoscaler,简称 HPA)实现负载自动扩缩容。

CoreDNS 的部署步骤

  • 从 CoreDNS 官方仓库下载最新的 YAML 配置文件模板,并生成新的配置文件
1
2
3
4
5
6
7
8
9
10
11
12
# 下载配置文件模板
# wget https://raw.githubusercontent.com/coredns/deployment/master/kubernetes/coredns.yaml.sed -O coredns.yaml.sed

# 替换配置文件模板中的占位符,生成新的配置文件
# sed -e "s/CLUSTER_DOMAIN/cluster.local/g" \
-e "s/REVERSE_CIDRS/in-addr.arpa ip6.arpa/g" \
-e "s/UPSTREAMNAMESERVER/8.8.8.8/g" \
-e "s/}STUBDOMAINS/}/" \
coredns.yaml.sed > coredns.yaml

# 移动生成的配置文件
# mv coredns.yaml /opt/kubernetes/cfg/
  • 检查 Kubelet 的 YAML 配置文件(kubelet-config.yml),确保包含以下配置信息(注意,Kubernetes 默认的 DNS Service IP 是 10.0.0.10,也可以指定为实际规划的 Pod 网段内的 IP,本文使用自定义的 IP)
1
2
# 编辑 Kubelet 的 YAML 配置文件,确保包含以下配置信息
# vim /opt/kubernetes/cfg/kubelet-config.yml
1
2
3
clusterDNS:
- 10.0.0.2 # 指向 CoreDNS Service 的 ClusterIP
clusterDomain: cluster.local # 默认域名后缀
  • 修改 CoreDNS 的 YAML 配置文件(coredns.yaml),只需要修改 spec.replicasspec.clusterIP 这两个配置
1
2
# 编辑 CoreDNS 的 YAML 配置文件,更改以下两个配置项
# vim /opt/kubernetes/cfg/coredns.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
......

apiVersion: apps/v1
kind: Deployment
metadata:
name: coredns
namespace: kube-system
labels:
k8s-app: kube-dns
kubernetes.io/name: "CoreDNS"
app.kubernetes.io/name: coredns
spec:
replicas: 3 # 指定 CoreDNS 的副本数量
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1

......
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
......

apiVersion: v1
kind: Service
metadata:
name: kube-dns
namespace: kube-system
annotations:
prometheus.io/port: "9153"
prometheus.io/scrape: "true"
labels:
k8s-app: kube-dns
kubernetes.io/cluster-service: "true"
kubernetes.io/name: "CoreDNS"
app.kubernetes.io/name: coredns
spec:
selector:
k8s-app: kube-dns
app.kubernetes.io/name: coredns
clusterIP: 10.0.0.2 # 指定 CoreDNS Service 的 ClusterIP

......
  • 应用 CoreDNS 的 YAML 配置文件
1
# kubectl apply -f /opt/kubernetes/cfg/coredns.yaml
  • 验证 CoreDNS 的 Pod 是否启动成功
1
# kubectl get pods -n kube-system -l k8s-app=kube-dns
1
2
3
4
NAME                       READY   STATUS    RESTARTS   AGE
coredns-6b9bb479b9-gpvvw 1/1 Running 0 21s
coredns-6b9bb479b9-l7z5b 1/1 Running 0 21s
coredns-6b9bb479b9-nzm5c 1/1 Running 0 21s
  • 若 CoreDNS 的 Pod 启动失败,可以通过查看日志信息来定位问题
1
# kubectl logs coredns-6b9bb479b9-lfd7j -n kube-system
  • 若希望删除 CoreDNS 的所有资源(包括 Pod、Controller、Service 等),可以执行以下命令
1
# kubectl delete -f /opt/kubernetes/cfg/coredns.yaml

多 Master 集群搭建问题

集群启动顺序问题

Kubernetes 集群包含多个组件,这些组件之间存在依赖关系。例如,大多数组件都依赖 API Server,因此 API Server 通常需要优先启动,而 API Server 本身又依赖 Etcd 集群先行启动。建议按以下顺序启动各个组件,否则有可能导致某些组件无法正常启动(尤其各个组件都是通过 Systemd 管理服务,并且集群的所有服务器都发生断电重启的情况下):

  • 1、Etcd 集群(所有集群节点的 Etcd)
  • 2、Master 节点组件:
    • (1) API Server
    • (2) Controller Manager
    • (3) Scheduler
  • 3、Node 节点组件:
    • (1) Kubelet
    • (2) Kube-Proxy
  • 4、网络插件(如 Flannel、Calico、Cilium 等)
  • 5、其他附加组件(如 CoreDNS、Ingress Controller、Dashboard 等)

Kubernetes 集群各个组件之间的依赖关系

  • Etcd 集群:Kubernetes 的数据存储后端,保存 API Server 的所有配置信息和资源对象;若未启动,API Server 无法连接存储并会报错退出。
  • API Server(Master 节点):集群核心入口,Controller Manager、Scheduler、kubectl 等都通过它访问集群状态;必须先启动才能让其他组件连接工作。
  • Controller Manager(Master 节点):通过 API Server 访问和修改集群状态(如创建 Pod、更新节点信息);若 API Server 未启动,则会不断重试连接而启动失败。
  • Scheduler(Master 节点):通过 API Server 获取未调度的 Pod 列表并为其分配合适的 Node;依赖 API Server 正常运行。

手动解决启动顺序问题

特别注意

当所有 Kubernetes 集群服务器重启后(比如断电重启),部分 Kubernetes 组件可能无法正确启动,因为每个组件之间存在一定的依赖关系,这时候就需要人工介入处理。

管理 Master 节点的组件启动,以下所有操作都是仅在 Kubernetes 集群的所有 Master 节点上执行

  • 查看 Master 节点组件的运行状态
1
2
3
4
5
# systemctl status kube-apiserver
# systemctl status kube-controller-manager
# systemctl status kube-scheduler
# systemctl status kubelet
# systemctl status kube-proxy
  • 若在 Master 节点组件的运行状态中,发现有不可忽略的致命错误提示信息,则严格按以下顺序重启各个组件
1
2
3
4
5
# systemctl restart kube-apiserver
# systemctl restart kube-controller-manager
# systemctl restart kube-scheduler
# systemctl restart kubelet
# systemctl restart kube-proxy
  • 再次查看 Master 节点组件的运行状态
1
2
3
4
5
# systemctl status kube-apiserver
# systemctl status kube-controller-manager
# systemctl status kube-scheduler
# systemctl status kubelet
# systemctl status kube-proxy

管理 Node 节点的组件启动,以下所有操作分别在 Kubernetes 集群的所有 Node 节点上执行

  • 查看 Node 节点组件的运行状态
1
2
# systemctl status kubelet
# systemctl status kube-proxy
  • 若在 Node 节点组件的运行状态中,发现有不可忽略的致命错误提示信息,则严格按以下顺序重启各个组件
1
2
# systemctl restart kubelet
# systemctl restart kube-proxy
  • 再次查看 Node 节点组件的运行状态
1
2
# systemctl status kubelet
# systemctl status kube-proxy

彻底解决启动顺序问题

特别注意

若每次在所有 Kubernetes 集群服务器重启后(比如断电重启),都需要手动按顺序重启每个 Kubernetes 组件,这将非常繁琐。为了彻底解决这个问题,可以使用 Systemd 的 RequiresAfterExecStartPre 特性来控制多个服务之间的启动顺序。

生成 Health-Check 的证书,用于检测 API Server 的健康状态,以下所有操作都是仅在 Kubernetes 集群的任意一个 Master 节点上执行

  • 在任意一个 Master 节点中,进入证书目录
1
2
# 进入证书目录
cd ~/tls/k8s/
  • 在任意一个 Master 节点中,创建用于生成 Health-Check 证书的配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# cat > health-check-csr.json << EOF
{
"CN": "health-check",
"hosts": [],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"O": "system:health-checkers"
}
]
}
EOF
  • 在任意一个 Master 节点中,使用自签 CA 签发 Health-Check 证书
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 查看当前目录下的文件
# ls
ca-config.json ca.pem kube-apiserver-key.pem kube-controller-manager-key.pem kube-proxy-key.pem kube-scheduler-key.pem
ca.csr health-check-csr.json kube-apiserver.pem kube-controller-manager.pem kube-proxy.pem kube-scheduler.pem
ca-csr.json kube-apiserver.csr kube-controller-manager.csr kube-proxy.csr kube-scheduler.csr
ca-key.pem kube-apiserver-csr.json kube-controller-manager-csr.json kube-proxy-csr.json kube-scheduler-csr.json

# 使用自签 CA 证书签发 Health-Check 证书,"-profile" 参数的值必须与 `ca-config.json` 配置文件中的值一致
# cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes health-check-csr.json | cfssljson -bare health-check

# 查看生成的 Health-Check 证书
# ls health-check*.pem
health-check-key.pem health-check.pem

# 验证生成的 Health-Check 证书
# cfssl-certinfo -cert health-check.pem
# openssl x509 -noout -text -in health-check.pem
  • 在任意一个 Master 节点中,将上面生成的 Health-Check 证书拷贝到本地目录里面
1
2
# 将 Health-Check 证书拷贝到本地目录里面
# cp ~/tls/k8s/health*.pem /opt/kubernetes/ssl/
  • 在任意一个 Master 节点中,将上面生成的 Health-Check 证书拷贝到其他 Master 节点里面
1
2
# 拷贝 Health-Check 证书到其他 Master 节点里面
# scp ~/tls/k8s/health-check*.pem root@k8s-master2:/opt/kubernetes/ssl/
  • 在任意一个 Master 节点中,将上面生成的 Health-Check 证书拷贝到所有 Node 节点里面
1
2
3
4
# 拷贝 Health-Check 证书到所有 Node 节点里面
# scp ~/tls/k8s/health-check*.pem root@k8s-node1:/opt/kubernetes/ssl/
# scp ~/tls/k8s/health-check*.pem root@k8s-node2:/opt/kubernetes/ssl/
# scp ~/tls/k8s/health-check*.pem root@k8s-node3:/opt/kubernetes/ssl/
  • 在任意一个 Master 节点中,创建 ClusterRole 的配置文件(仅授予只读的健康检测权限)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 创建配置文件
# cat > /opt/kubernetes/cfg/health-check-role.yaml << EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: health-check-role
rules:
- nonResourceURLs:
- /healthz
- /livez
- /readyz
verbs:
- get
EOF
  • 在任意一个 Master 节点中,创建绑定用户到 ClusterRole 的配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建配置文件
# cat > /opt/kubernetes/cfg/health-check-binding.yaml << EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: health-check-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: health-check-role
subjects:
- kind: User
name: health-check
apiGroup: rbac.authorization.k8s.io
EOF
  • 在任意一个 Master 节点中,应用 YAML 配置文件
1
2
3
4
5
# 应用 ClusterRole(仅授予只读的健康检测权限)
# kubectl apply -f /opt/kubernetes/cfg/health-check-role.yaml

# 应用 ClusterRoleBinding(将用户绑定到指定的集群角色)
# kubectl apply -f /opt/kubernetes/cfg/health-check-binding.yaml
  • 在所有节点(包括 Master 和 Node)中,分别验证 API Server 健康检测接口的调用(请自行更改 API Server 的地址,需要使用 Keepalived 的 VIP 和 HaProxy 的反向代理端口,切勿直接拷贝执行以下命令
1
2
# 若接口可以正常调用,则会返回 ok 字符串
# curl --silent --fail --cacert /opt/kubernetes/ssl/ca.pem --cert /opt/kubernetes/ssl/health-check.pem --key /opt/kubernetes/ssl/health-check-key.pem https://192.168.2.100:16443/readyz

管理 Master 节点的组件启动,以下所有操作都是仅在 Kubernetes 集群的所有 Master 节点上执行

  • 在所有 Master 节点中,下载 Etcd 的 etcdctl 可执行文件(如果本地不存在,才需要下载可执行文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看是否存在etcdctl可执行文件(如果存在,则不需要再下载)
# ls /opt/etcd/bin/etcdctl

# 下载二进制包
# wget https://github.com/etcd-io/etcd/releases/download/v3.4.9/etcd-v3.4.9-linux-amd64.tar.gz

# 创建目录
# mkdir -p /opt/etcd/bin

# 解压文件
# tar zxvf etcd-v3.4.9-linux-amd64.tar.gz

# 移动文件
# mv etcd-v3.4.9-linux-amd64/etcdctl /opt/etcd/bin/
  • 在所有 Master 节点中,重写(覆盖)API Server 服务的 Systemd 配置文件(请自行更改 Etcd 集群节点的 IP 和端口,切勿直接拷贝执行以下命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 重写 API Server 服务管理的配置文件
# cat > /usr/lib/systemd/system/kube-apiserver.service << EOF
[Unit]
Description=Kubernetes API Server
Documentation=https://github.com/kubernetes/kubernetes
After=network-online.target

[Service]
EnvironmentFile=/opt/kubernetes/cfg/kube-apiserver.conf
ExecStartPre=/bin/bash -c 'until ETCDCTL_API=3 /opt/etcd/bin/etcdctl --endpoints=https://192.168.2.191:2379,https://192.168.2.112:2379,https://192.168.2.131:2379 --cacert=/opt/etcd/ssl/ca.pem --cert=/opt/etcd/ssl/server.pem --key=/opt/etcd/ssl/server-key.pem endpoint status --write-out=table | grep -v "^+" | grep -q " true "; do echo "Waiting for Etcd cluster to be ready..."; sleep 2; done && echo "Etcd cluster is now ready."'
ExecStart=/opt/kubernetes/bin/kube-apiserver \$KUBE_APISERVER_OPTS
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
1
2
3
4
5
6
7
8
# 更新系统配置
# systemctl daemon-reload

# 重启 API Server 服务
# systemctl restart kube-apiserver

# 查看 API Server 服务的运行状态
# systemctl status kube-apiserver
  • 在所有 Master 节点中,重写(覆盖)Controller Manager 服务的 Systemd 配置文件(请自行更改 API Server 的地址,需要使用 Keepalived 的 VIP 和 HaProxy 的反向代理端口,切勿直接拷贝执行以下命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 重写 Controller Manager 服务管理的配置文件
# cat > /usr/lib/systemd/system/kube-controller-manager.service << EOF
[Unit]
Description=Kubernetes Controller Manager
Documentation=https://github.com/kubernetes/kubernetes
After=network-online.target

[Service]
EnvironmentFile=/opt/kubernetes/cfg/kube-controller-manager.conf
ExecStartPre=/bin/bash -c 'until curl --silent --fail --cacert /opt/kubernetes/ssl/ca.pem --cert /opt/kubernetes/ssl/health-check.pem --key /opt/kubernetes/ssl/health-check-key.pem https://192.168.2.100:16443/readyz > /dev/null 2>&1; do echo "Waiting for API Server to be ready..."; sleep 2; done && echo "API Server is now ready."'
ExecStart=/opt/kubernetes/bin/kube-controller-manager \$KUBE_CONTROLLER_MANAGER_OPTS
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
1
2
3
4
5
6
7
8
# 更新系统配置
# systemctl daemon-reload

# 重启 Controller Manager 服务
# systemctl restart kube-controller-manager

# 查看 Controller Manager 服务的运行状态
# systemctl status kube-controller-manager
  • 在所有 Master 节点中,重写(覆盖)Scheduler 服务的 Systemd 配置文件(请自行更改 API Server 的地址,需要使用 Keepalived 的 VIP 和 HaProxy 的反向代理端口,切勿直接拷贝执行以下命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 重写 Scheduler 服务管理的配置文件
# cat > /usr/lib/systemd/system/kube-scheduler.service << EOF
[Unit]
Description=Kubernetes Scheduler
Documentation=https://github.com/kubernetes/kubernetes
After=network-online.target

[Service]
EnvironmentFile=/opt/kubernetes/cfg/kube-scheduler.conf
ExecStartPre=/bin/bash -c 'until curl --silent --fail --cacert /opt/kubernetes/ssl/ca.pem --cert /opt/kubernetes/ssl/health-check.pem --key /opt/kubernetes/ssl/health-check-key.pem https://192.168.2.100:16443/readyz > /dev/null 2>&1; do echo "Waiting for API Server to be ready..."; sleep 2; done && echo "API Server is now ready."'
ExecStart=/opt/kubernetes/bin/kube-scheduler \$KUBE_SCHEDULER_OPTS
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
1
2
3
4
5
6
7
8
# 更新系统配置
# systemctl daemon-reload

# 重启 Scheduler 服务
# systemctl restart kube-scheduler

# 查看 Scheduler 服务的运行状态
# systemctl status kube-scheduler
  • 在所有 Master 节点中,重写(覆盖)Kubelet 服务的 Systemd 配置文件(请自行更改 API Server 的地址,需要使用 Keepalived 的 VIP 和 HaProxy 的反向代理端口,切勿直接拷贝执行以下命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 重写 Kubelet 服务管理的配置文件
# cat > /usr/lib/systemd/system/kubelet.service << EOF
[Unit]
Description=Kubernetes Kubelet
Documentation=https://github.com/kubernetes/kubernetes
After=network-online.target
After=docker.service

[Service]
EnvironmentFile=/opt/kubernetes/cfg/kubelet.conf
ExecStartPre=/bin/bash -c 'until curl --silent --fail --cacert /opt/kubernetes/ssl/ca.pem --cert /opt/kubernetes/ssl/health-check.pem --key /opt/kubernetes/ssl/health-check-key.pem https://192.168.2.100:16443/readyz > /dev/null 2>&1; do echo "Waiting for API Server to be ready..."; sleep 2; done && echo "API Server is now ready."'
ExecStart=/opt/kubernetes/bin/kubelet \$KUBELET_OPTS
Restart=on-failure
RestartSec=5
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF
1
2
3
4
5
6
7
8
# 更新系统配置
# systemctl daemon-reload

# 重启 Kubelet 服务
# systemctl restart kubelet

# 查看 Kubelet 服务的运行状态
# systemctl status kubelet
  • 在所有 Master 节点中,重写(覆盖)Kube-Proxy 服务的 Systemd 配置文件(请自行更改 API Server 的地址,需要使用 Keepalived 的 VIP 和 HaProxy 的反向代理端口,切勿直接拷贝执行以下命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 重写 Kube-Proxy 服务管理的配置文件
# cat > /usr/lib/systemd/system/kube-proxy.service << EOF
[Unit]
Description=Kubernetes Proxy
Documentation=https://github.com/kubernetes/kubernetes
After=network-online.target
After=docker.service

[Service]
EnvironmentFile=/opt/kubernetes/cfg/kube-proxy.conf
ExecStartPre=/bin/bash -c 'until curl --silent --fail --cacert /opt/kubernetes/ssl/ca.pem --cert /opt/kubernetes/ssl/health-check.pem --key /opt/kubernetes/ssl/health-check-key.pem https://192.168.2.100:16443/readyz > /dev/null 2>&1; do echo "Waiting for API Server to be ready..."; sleep 2; done && echo "API Server is now ready."'
ExecStart=/opt/kubernetes/bin/kube-proxy \$KUBE_PROXY_OPTS
Restart=on-failure
RestartSec=5
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF
1
2
3
4
5
6
7
8
# 更新系统配置
# systemctl daemon-reload

# 重启 Kube-Proxy 服务
# systemctl restart kube-proxy

# 查看 Kube-Proxy 服务的运行状态
# systemctl status kube-proxy

管理 Node 节点的组件启动,以下所有操作分别在 Kubernetes 集群的所有 Node 节点上执行

  • 在所有 Node 节点中,重写(覆盖)Kubelet 服务的 Systemd 配置文件(请自行更改 API Server 的地址,需要使用 Keepalived 的 VIP 和 HaProxy 的反向代理端口,切勿直接拷贝执行以下命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 重写 Kubelet 服务管理的配置文件
# cat > /usr/lib/systemd/system/kubelet.service << EOF
[Unit]
Description=Kubernetes Kubelet
Documentation=https://github.com/kubernetes/kubernetes
After=network-online.target
After=docker.service

[Service]
EnvironmentFile=/opt/kubernetes/cfg/kubelet.conf
ExecStartPre=/bin/bash -c 'until curl --silent --fail --cacert /opt/kubernetes/ssl/ca.pem --cert /opt/kubernetes/ssl/health-check.pem --key /opt/kubernetes/ssl/health-check-key.pem https://192.168.2.100:16443/readyz > /dev/null 2>&1; do echo "Waiting for API Server to be ready..."; sleep 2; done && echo "API Server is now ready."'
ExecStart=/opt/kubernetes/bin/kubelet \$KUBELET_OPTS
Restart=on-failure
RestartSec=5
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF
1
2
3
4
5
6
7
8
# 更新系统配置
# systemctl daemon-reload

# 重启 Kubelet 服务
# systemctl restart kubelet

# 查看 Kubelet 服务的运行状态
# systemctl status kubelet
  • 在所有 Node 节点中,重写(覆盖)Kube-Proxy 服务的 Systemd 配置文件(请自行更改 API Server 的地址,需要使用 Keepalived 的 VIP 和 HaProxy 的反向代理端口,切勿直接拷贝执行以下命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 重写 Kube-Proxy 服务管理的配置文件
# cat > /usr/lib/systemd/system/kube-proxy.service << EOF
[Unit]
Description=Kubernetes Proxy
Documentation=https://github.com/kubernetes/kubernetes
After=network-online.target
After=docker.service

[Service]
EnvironmentFile=/opt/kubernetes/cfg/kube-proxy.conf
ExecStartPre=/bin/bash -c 'until curl --silent --fail --cacert /opt/kubernetes/ssl/ca.pem --cert /opt/kubernetes/ssl/health-check.pem --key /opt/kubernetes/ssl/health-check-key.pem https://192.168.2.100:16443/readyz > /dev/null 2>&1; do echo "Waiting for API Server to be ready..."; sleep 2; done && echo "API Server is now ready."'
ExecStart=/opt/kubernetes/bin/kube-proxy \$KUBE_PROXY_OPTS
Restart=on-failure
RestartSec=5
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF
1
2
3
4
5
6
7
8
# 更新系统配置
# systemctl daemon-reload

# 重启 Kube-Proxy 服务
# systemctl restart kube-proxy

# 查看 Kube-Proxy 服务的运行状态
# systemctl status kube-proxy

Etcd 无法正常启动

  • Etcd 集群断电重启后,通过 systemctl status etcd 命令查看 Etcd 节点(比如:节点名称是 etcd-3,IP 是 192.168.2.131)的运行状态时,提示 Raft 同步失败(如下)。最有可能的原因是这个 Etcd 节点的数据目录损坏或不一致导致,解决方案如下:
1
2
3
4
5
8月 07 09:43:32 k8s-node2 etcd[6043]: {"level":"info","ts":"2025-08-07T09:43:32.007+0800","caller":"rafthttp/stream.go:425","msg":"established TCP streaming connection with remote peer","stream-reader-type":"stream MsgApp v2","local-member-id":"9cb4eb07d38510b5","remote-peer-id":"a00042f5519886e3"}
8月 07 09:43:32 k8s-node2 etcd[6043]: {"level":"info","ts":"2025-08-07T09:43:32.026+0800","caller":"etcdserver/server.go:715","msg":"initialized peer connections; fast-forwarding election ticks","local-member-id":"9cb4eb07d38510b5","forward-ticks":8,"forward-duration":"800ms","election-ticks":10,"election-timeout...
8月 07 09:43:38 k8s-node2 etcd[6043]: {"level":"warn","ts":"2025-08-07T09:43:38.988+0800","caller":"etcdserver/server.go:2065","msg":"failed to publish local member to cluster through raft","local-member-id":"9cb4eb07d38510b5","local-member-attributes":"{Name:etcd-3 ClientURLs:[https://192.168.2.131:2379]}","requ...
8月 07 09:43:45 k8s-node2 etcd[6043]: {"level":"warn","ts":"2025-08-07T09:43:45.988+0800","caller":"etcdserver/server.go:2065","msg":"failed to publish local member to cluster through raft","local-member-id":"9cb4eb07d38510b5","local-member-attributes":"{Name:etcd-3 ClientURLs:[https://192.168.2.131:2379]}","requ...
Hint: Some lines were ellipsized, use -l to show in full.
  • 在其他健康的 Etcd 节点上,查看 Etcd 集群的运行状态,可以发现缺少了 etcd-3 节点
1
2
# 查看集群的运行状态
# /opt/etcd/bin/etcdctl --endpoints=https://192.168.2.191:2379,https://192.168.2.112:2379,https://192.168.2.131:2379 --cacert=/opt/etcd/ssl/ca.pem --cert=/opt/etcd/ssl/server.pem --key=/opt/etcd/ssl/server-key.pem endpoint status --write-out=table
1
2
3
4
5
6
+----------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS |
+----------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| https://192.168.2.191:2379 | 9a8c44f9310511 | 3.4.9 | 5.0 MB | false | false | 369 | 314583 | 314583 | |
| https://192.168.2.112:2379 | a00042f5519886e3 | 3.4.9 | 5.0 MB | true | false | 369 | 314583 | 314583 | |
+----------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
  • 通过其他健康的 Etcd 节点,查看 Etcd 集群的成员列表,虽然 etcd-3 节点无法正常启动,但依旧可以看到 etcd-3 节点
1
2
# 查看集群的成员列表
# /opt/etcd/bin/etcdctl --endpoints=https://192.168.2.191:2379 --cacert=/opt/etcd/ssl/ca.pem --cert=/opt/etcd/ssl/server.pem --key=/opt/etcd/ssl/server-key.pem member list
1
2
3
9a8c44f9310511, started, etcd-1, https://192.168.2.191:2380, https://192.168.2.191:2379, false
5e5716f1d7eba042, started, etcd-3, https://192.168.2.131:2380, https://192.168.2.131:2379, false
a00042f5519886e3, started, etcd-2, https://192.168.2.112:2380, https://192.168.2.112:2379, false
  • 在其他健康的 Etcd 节点上,将无法正常启动的 etcd-3 节点移除出集群
1
2
# 将节点移除出集群
# /opt/etcd/bin/etcdctl --endpoints=https://192.168.2.191:2379 --cacert=/opt/etcd/ssl/ca.pem --cert=/opt/etcd/ssl/server.pem --key=/opt/etcd/ssl/server-key.pem member remove 5e5716f1d7eba042
  • 在无法正常启动的 etcd-3 节点上,删除本地的 Etcd 数据目录
1
2
3
4
5
# 关闭Etcd服务
# systemctl stop etcd

# 删除Etcd服务的数据目录
# rm -rf /var/lib/etcd/default.etcd
  • 通过其他健康的 Etcd 节点,将无法正常启动的 etcd-3 节点重新加入 Etcd 集群
1
2
# 将节点重新加入集群
# /opt/etcd/bin/etcdctl --endpoints=https://192.168.2.112:2379 --cacert=/opt/etcd/ssl/ca.pem --cert=/opt/etcd/ssl/server.pem --key=/opt/etcd/ssl/server-key.pem member add etcd-3 --peer-urls=https://192.168.2.131:2380
  • 执行命令将 etcd-3 节点重新加入 Etcd 集群后,会输出以下内容
1
2
3
4
ETCD_NAME="etcd-3"
ETCD_INITIAL_CLUSTER="etcd-1=https://192.168.2.191:2380,etcd-3=https://192.168.2.131:2380,etcd-2=https://192.168.2.112:2380"
ETCD_INITIAL_ADVERTISE_PEER_URLS="https://192.168.2.131:2380"
ETCD_INITIAL_CLUSTER_STATE="existing"
  • 在无法正常启动的 etcd-3 节点上,更改本地的 Etcd 配置文件,将 ETCD_INITIAL_CLUSTER_STATE 改为 existing,并重启 Etcd 服务(如果上面的操作都生效了,那么这时候 Etcd 服务应该就可以正常启动了)
1
2
3
4
5
6
7
8
# 更改Etcd的配置文件
# sed -i 's/^ETCD_INITIAL_CLUSTER_STATE="new"/ETCD_INITIAL_CLUSTER_STATE="existing"/' /opt/etcd/cfg/etcd.conf

# 重启Etcd服务
# systemctl restart etcd

# 查看Etcd服务的运行状态
# systemctl status etcd
  • 在之前无法正常启动的 etcd-3 节点上,还原本地的 Etcd 配置文件,将 ETCD_INITIAL_CLUSTER_STATE 改为 new,并重启 Etcd 服务
1
2
3
4
5
6
7
8
# 更改Etcd的配置文件
# sed -i 's/^ETCD_INITIAL_CLUSTER_STATE="existing"/ETCD_INITIAL_CLUSTER_STATE="new"/' /opt/etcd/cfg/etcd.conf

# 重启Etcd服务
# systemctl restart etcd

# 查看Etcd服务的运行状态
# systemctl status etcd
  • 在其他健康的 Etcd 节点上,再次查看 Etcd 集群的运行状态,可以发现 etcd-3 节点(会获得新的节点 ID)正常运行
1
2
# 查看集群的运行状态
# /opt/etcd/bin/etcdctl --endpoints=https://192.168.2.191:2379,https://192.168.2.112:2379,https://192.168.2.131:2379 --cacert=/opt/etcd/ssl/ca.pem --cert=/opt/etcd/ssl/server.pem --key=/opt/etcd/ssl/server-key.pem endpoint status --write-out=table
1
2
3
4
5
6
7
+----------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS |
+----------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| https://192.168.2.191:2379 | 9a8c44f9310511 | 3.4.9 | 5.0 MB | false | false | 369 | 323848 | 323848 | |
| https://192.168.2.112:2379 | a00042f5519886e3 | 3.4.9 | 5.0 MB | true | false | 369 | 323848 | 323848 | |
| https://192.168.2.131:2379 | d282ba1f56161393 | 3.4.9 | 5.0 MB | false | false | 369 | 323848 | 323848 | |
+----------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+

问题分析与总结

ETCD_INITIAL_CLUSTER_STATE="new" 表示 Etcd 节点将以 "全新集群" 的方式启动,此时它会根据 ETCD_INITIAL_CLUSTER 中的成员列表初始化集群,并假设 Raft 日志为空。如果该节点此前已经加入过集群,当再次使用 new 启动时,就会导致与现有集群中的成员 ID 冲突,触发 Raft 同步失败。而 ETCD_INITIAL_CLUSTER_STATE="existing" 则用于已存在的集群节点,表示该节点将从其他成员处拉取 Raft 日志进行同步。因此,当 etcd-3 节点已经出现在 Etcd 集群的成员列表中时,配置 ETCD_INITIAL_CLUSTER_STATE="new" 是错误的,应该改为 existing,否则该节点在重启时会错误地尝试初始化一个新的集群,导致 Raft 冲突和无法加入集群。简而言之,在初次搭建 Etcd 集群时,ETCD_INITIAL_CLUSTER_STATE="new" 是正确的,但在 Etcd 节点重启或者重新加入集群时,如果这个值仍然是 new,那就可能会引发严重的问题。

连接 API Server 失败

Kubernetes 部分组件(比如 Controller Manager)在启动后,无法与 API Server 通过 HTTPS 协议进行加密通信,出现以下错误信息:

1
2
3
4
5
6
11月 11 11:56:48 k8s-master1 kube-controller-manager[7582]: I1111 11:56:48.590181    7582 leaderelection.go:243] attempting to acquire leader lease  kube-system/kube-controller-manager...
11月 11 11:56:48 k8s-master1 kube-controller-manager[7582]: E1111 11:56:48.591986 7582 leaderelection.go:325] error retrieving resource lock kube-system/kube-controller-manager: the server rejected our request for an unknown reason (get endpoints kube-controller-manager)
11月 11 11:56:51 k8s-master1 kube-controller-manager[7582]: E1111 11:56:51.722757 7582 leaderelection.go:325] error retrieving resource lock kube-system/kube-controller-manager: the server rejected our request for an unknown reason (get endpoints kube-controller-manager)
11月 11 11:56:54 k8s-master1 kube-controller-manager[7582]: E1111 11:56:54.201575 7582 leaderelection.go:325] error retrieving resource lock kube-system/kube-controller-manager: the server rejected our request for an unknown reason (get endpoints kube-controller-manager)
11月 11 11:56:57 k8s-master1 kube-controller-manager[7582]: E1111 11:56:57.184837 7582 leaderelection.go:325] error retrieving resource lock kube-system/kube-controller-manager: the server rejected our request for an unknown reason (get endpoints kube-controller-manager)
11月 11 11:56:59 k8s-master1 kube-controller-manager[7582]: E1111 11:56:59.859566 7582 leaderelection.go:325] error retrieving resource lock kube-system/kube-controller-manager: the server rejected our request for an unknown reason (get endpoints kube-controller-manager)

这通常是 Kubernetes 组件与 API Server 通信时,所有使用的 CA 证书不匹配导致(或者缺少 CA 证书配置),这里以 Controller Manager 组件为例,给出解决方法


更改 Controller Manager 的配置文件,使其启动后打印详细的日志信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 更改配置文件,将 --logtostderr 配置项的值改为 true,打印详细的日志信息
# cat > /opt/kubernetes/cfg/kube-controller-manager.conf << EOF
KUBE_CONTROLLER_MANAGER_OPTS="--logtostderr=true \\
--v=2 \\
--log-dir=/opt/kubernetes/logs \\
--leader-elect=true \\
--master=192.168.2.100:16443 \\
--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=/opt/kubernetes/ssl/ca.pem \\
--cluster-signing-key-file=/opt/kubernetes/ssl/ca-key.pem \\
--root-ca-file=/opt/kubernetes/ssl/ca.pem \\
--service-account-private-key-file=/opt/kubernetes/ssl/ca-key.pem \\
--use-service-account-credentials=true \\
--kubeconfig=/opt/kubernetes/cfg/kube-controller-manager.kubeconfig \\
--cluster-signing-duration=87600h0m0s"
EOF

重启 Controller Manager 服务,查看输出的详细日志信息,以此定位问题

1
2
3
4
5
6
7
8
9
10
11
# 更新系统配置
# systemctl daemon-reload

# 重启服务
# systemctl restart kube-controller-manager

# 查看运行状态
# systemctl status kube-controller-manager

# 查看日志信息
# journalctl -u kube-controller-manager -f

手动测试与 API Server 的连接和认证

1
2
3
4
5
6
7
8
9
# 使用 Controller Manager 的 kubeconfig 测试连接
# kubectl --kubeconfig=/opt/kubernetes/cfg/kube-controller-manager.kubeconfig get componentstatuses

# 如果测试连接失败,可以尝试直接使用 Controller Manager 的证书进行测试
# curl -k \
--cert /opt/kubernetes/ssl/controller-manager.pem \
--key /opt/kubernetes/ssl/controller-manager-key.pem \
--cacert /opt/kubernetes/ssl/ca.pem \
https://192.168.2.100:16443/version

检查 API Server 的证书,必须包含 Keepalived 的虚拟 IP 地址(比如 192.168.2.100

1
2
# 检查 API Server 证书的 SAN
# openssl x509 -in /opt/kubernetes/ssl/server.pem -text -noout | grep -A 5 "X509v3 Subject Alternative Name"

检查 Controller Manager 证书中的 Subject 和 SAN

1
2
# 检查证书的 Subject 和 SAN
# openssl x509 -in /opt/kubernetes/ssl/controller-manager.pem -text -noout | grep -A 10 "Subject:"

检查 HaProxy 与 Keepalived 是否可以正常工作

1
2
# 验证 Keepalived 的 VIP 与 HaProxy 的反向代理功能(注意,这里即使测试通过,只能说明网络连通性没问题,不能说明其他组件一定可以 API Server 通过 HTTPS 协议进行加密通信,尤其是涉及到证书问题时)
# curl --cacert /opt/kubernetes/ssl/ca.pem https://192.168.2.100:16443/healthz

经检查,发现 Controller Manager 缺少了以下配置项,尤其是缺少 --requestheader-client-ca-file 配置项,导致无法与 API Server 通过 HTTPS 协议进行加密通信(使用 CA 证书)

1
2
3
4
5
6
--authentication-kubeconfig=/opt/kubernetes/cfg/kube-controller-manager.kubeconfig \\
--authorization-kubeconfig=/opt/kubernetes/cfg/kube-controller-manager.kubeconfig \\
--client-ca-file=/opt/kubernetes/ssl/ca.pem \\
--requestheader-client-ca-file=/opt/kubernetes/ssl/ca.pem \\
--tls-cert-file=/opt/kubernetes/ssl/controller-manager.pem \\
--tls-private-key-file=/opt/kubernetes/ssl/controller-manager-key.pem \\

最终,Controller Manager 使用以下配置信息后,可以与 API Server 通过 HTTPS 协议进行加密通信(使用 CA 证书)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# cat > /opt/kubernetes/cfg/kube-controller-manager.conf << EOF
KUBE_CONTROLLER_MANAGER_OPTS="--logtostderr=false \\
--v=2 \\
--log-dir=/opt/kubernetes/logs \\
--leader-elect=true \\
--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 \\
--secure-port=10257 \\
--kubeconfig=/opt/kubernetes/cfg/kube-controller-manager.kubeconfig \\
--authentication-kubeconfig=/opt/kubernetes/cfg/kube-controller-manager.kubeconfig \\
--authorization-kubeconfig=/opt/kubernetes/cfg/kube-controller-manager.kubeconfig \\
--client-ca-file=/opt/kubernetes/ssl/ca.pem \\
--use-service-account-credentials=true \\
--service-account-private-key-file=/opt/kubernetes/ssl/ca-key.pem \\
--cluster-signing-cert-file=/opt/kubernetes/ssl/ca.pem \\
--cluster-signing-key-file=/opt/kubernetes/ssl/ca-key.pem \\
--requestheader-client-ca-file=/opt/kubernetes/ssl/ca.pem \\
--tls-cert-file=/opt/kubernetes/ssl/kube-controller-manager.pem \\
--tls-private-key-file=/opt/kubernetes/ssl/kube-controller-manager-key.pem \\
--root-ca-file=/opt/kubernetes/ssl/ca.pem \\
--controllers=*,bootstrapsigner,tokencleaner \\
--deployment-controller-sync-period=10s \\
--enable-garbage-collector=true \\
--terminated-pod-gc-threshold=50 \\
--node-monitor-period=5s \\
--node-monitor-grace-period=20s \\
--pod-eviction-timeout=2m0s \\
--cluster-signing-duration=87600h0m0s"
EOF

提示

配置项 --requestheader-client-ca-file 用于验证 API Server 聚合层(Aggregation Layer)客户端请求头证书的 CA 文件,Controller Manager 在与启用了聚合层的 API Server 通信时,会使用该 CA 验证代理请求来源的合法性,可以与 --requestheader-allowed-names--requestheader-extra-headers-prefix--requestheader-group-headers--requestheader-username-headers 等参数配合使用。

Kubelet 无法正常启动

Kubelet 服务断电重启后,通过 systemctl status kubelet 命令查看运行状态时,服务没有处于 active (running) 状态,并且有各种错误信息,尝试多种方法依旧无法正常重启。解决方案如下:

  • 在 Etcd 集群的所有节点上,分别删除 Etcd 的数据目录(切记谨慎操作,Etcd 数据删除后,已有的容器都可能无法正常启动,并且整个 Kubernetes 集群都可能需要重建),并重启 Etcd 服务
1
2
3
4
5
6
7
8
9
10
11
# 关闭Etcd服务
# systemctl stop etcd

# 删除Etcd服务的数据目录
# rm -rf /var/lib/etcd/default.etcd

# 启动Etcd服务(多个Etcd节点必须同时启动)
# systemctl start etcd

# 查看Etcd服务的运行状态
# systemctl status etcd
  • 在任意一个 Master 节点中,按顺序重启以下服务
1
2
3
4
5
# systemctl restart kube-apiserver
# systemctl restart kube-controller-manager
# systemctl restart kube-scheduler
# systemctl restart kubelet
# systemctl restart kube-proxy
  • 在所有 Node 节点中,按顺序重启以下服务
1
2
# systemctl restart kubelet
# systemctl restart kube-proxy
  • 在任意一个 Master 节点中,批准 Kubelet 证书签名请求并允许 Node 节点加入 Kubernetes 集群
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 查看 Kubernetes 集群中的所有证书签名请求
# kubectl get csr
NAME AGE SIGNERNAME REQUESTOR CONDITION
node-csr-B91KCv7T_4A4RCfvfEBfFOP3V5zCYKK5NQkVLDmyly4 86s kubernetes.io/kube-apiserver-client-kubelet kubelet-bootstrap Pending
node-csr-F4zdBAJxxKnDLsWJbYHVi_XKF8Uwh1MMarXNJiHAUy0 85s kubernetes.io/kube-apiserver-client-kubelet kubelet-bootstrap Pending
node-csr-GYbsPLYySA0LiUkuhbpXlApj-JC-TDnA7SVRPHWU2mI 88s kubernetes.io/kube-apiserver-client-kubelet kubelet-bootstrap Pending
node-csr-bLUS_w2or-Rq9c5JgCzaf5e6QYHND6PTEqQFTvDsY5g 96s kubernetes.io/kube-apiserver-client-kubelet kubelet-bootstrap Pending
node-csr-ndBBynU94khVf6ADv4pdmtrlsxDOXAnyxu18e_VnhBU 87s kubernetes.io/kube-apiserver-client-kubelet kubelet-bootstrap Pending

# 批准证书签名请求(请根据上面输出的证书签名请求名称,自行更改命令行参数)
# kubectl certificate approve node-csr-B91KCv7T_4A4RCfvfEBfFOP3V5zCYKK5NQkVLDmyly4
# kubectl certificate approve node-csr-F4zdBAJxxKnDLsWJbYHVi_XKF8Uwh1MMarXNJiHAUy0
# kubectl certificate approve node-csr-GYbsPLYySA0LiUkuhbpXlApj-JC-TDnA7SVRPHWU2mI
# kubectl certificate approve node-csr-bLUS_w2or-Rq9c5JgCzaf5e6QYHND6PTEqQFTvDsY5g
# kubectl certificate approve node-csr-ndBBynU94khVf6ADv4pdmtrlsxDOXAnyxu18e_VnhBU

# 查看集群中所有节点的状态(当 CNI 网络插件部署完成后,所有节点都会自动切换到准备就绪状态 - Ready)
# kubectl get node
NAME STATUS ROLES AGE VERSION
k8s-master1 NotReady <none> 10m v1.19.10
k8s-master2 NotReady <none> 18s v1.19.10
k8s-node1 NotReady <none> 29s v1.19.10
k8s-node2 NotReady <none> 22s v1.19.10
k8s-node3 NotReady <none> 25s v1.19.10

Kubelet 发送 CSR 请求失败

Kubelet 服务启动后,无法正常向 API Server 发起 CSR(证书签名请求),解决方案如下:

  • 在当前节点上,清理 Kubelet 运行时产生的文件,并重启 Kubelet 服务
1
2
3
4
5
6
7
8
9
10
11
12
13
# 关闭 Kubelet 服务
# systemctl stop kubelet

# 删除 Kubelet 运行时残留的文件
# rm -rf /opt/kubernetes/ssl/kubelet*
# rm -rf /opt/kubernetes/cfg/kubelet.kubeconfig

# 检查 Kubelet 是否有可用的 CA 证书
# ls /opt/kubernetes/ssl/ca*.pem
/opt/kubernetes/ssl/ca-key.pem /opt/kubernetes/ssl/ca.pem

# 启动 Kubelet 服务
# systemctl start kubelet
  • 在任意一个 Master 节点上,执行以下命令,批准 Kubelet 证书签名请求并允许 Node 节点加入 Kubernetes 集群
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 查看 Kubernetes 集群中的所有证书签名请求
# kubectl get csr
NAME AGE SIGNERNAME REQUESTOR CONDITION
node-csr-B91KCv7T_4A4RCfvfEBfFOP3V5zCYKK5NQkVLDmyly4 95s kubernetes.io/kube-apiserver-client-kubelet kubelet-bootstrap Pending

# 批准证书签名请求(请根据上面输出的证书签名请求名称,自行更改命令行参数)
# kubectl certificate approve node-csr-B91KCv7T_4A4RCfvfEBfFOP3V5zCYKK5NQkVLDmyly4

# 查看集群中所有节点的状态(当 CNI 网络插件部署完成后,所有节点都会自动切换到准备就绪状态 - Ready)
# kubectl get node
NAME STATUS ROLES AGE VERSION
k8s-master1 NotReady <none> 10m v1.19.10
k8s-master2 NotReady <none> 18s v1.19.10
k8s-node1 NotReady <none> 29s v1.19.10
k8s-node2 NotReady <none> 22s v1.19.10
k8s-node3 NotReady <none> 25s v1.19.10

CNI 网络插件无法正常部署

  • 在 Kubernetes 集群中部署 CNI 网络插件(Flannel)后,通过 systemctl status kubelet 命令查看 Kubelet 的运行状态时,出现以下镜像错误信息,导致 CNI 网络插件(Flannel)无法正常部署
1
2
3
4
k8s-node1 kubelet[30963]: E0805 19:42:19.257544   30963 kuberuntime_sandbox.go:69] CreatePodSandbox for pod "kube-flannel-ds-g5q5l_kube-flannel(0c854c29-c158-45ca-a6c4-5d9ee7d9879b)" failed: rpc error: code = Unknown desc = failed pulling image "lizhenliang/pause-amd64:3.0": Error response from daemon: Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
k8s-node1 kubelet[14733]: E0805 21:34:10.987901 14733 kubelet.go:2134] Container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized
k8s-node1 kubelet[14733]: E0805 21:34:15.991080 14733 kubelet.go:2134] Container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized
k8s-node1 kubelet[14733]: E0805 21:34:21.015325 14733 kubelet.go:2134] Container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized
  • 这是因为 Kubelet 会根据 kubelet.conf 配置文件,从 Docker Hub 拉取 lizhenliang/pause-amd64:3.0 镜像;若网络访问超时,就会出现上述错误信息。解决方案有这几种:手动拉取镜像、配置 Docker 代理、更换 Docker 镜像源,最后重启 Kubelet 服务
1
2
3
4
5
6
7
8
# 关闭 Kubelet 服务
# systemctl stop kubelet

# 手动拉取 Pause 镜像(用于 Pod 网络命名空间)
# docker pull lizhenliang/pause-amd64:3.0

# 启动 Kubelet 服务
# systemctl start kubelet
  • 当 Kubelet 服务成功启动,且 CNI 网络插件(Flannel)成功部署后,可以发现当前节点会有以下 Docker 镜像(缺一不可)
1
2
3
4
5
6
# 查看镜像列表
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ghcr.io/flannel-io/flannel v0.27.4 e83704a17731 5 weeks ago 91.4MB
ghcr.io/flannel-io/flannel-cni-plugin v1.8.0-flannel1 bb28ded63816 5 weeks ago 10.8MB
lizhenliang/pause-amd64 3.0 99e59f495ffa 9 years ago 747kB

参考资料