三台 CentOS 7 主机,用二进制方式搭一套 Kubernetes v1.17.1 高可用集群:3 个 Master、3 个 Node、3 个 etcd 成员混部在同一批机器上。选二进制而不是 kubeadm,是为了让证书、启动参数、systemd unit 都掌握在自己手里——组件起不来时能直接看到命令行,而不是去翻被生成的临时配置。高可用的 etcd 和 apiserver 都要求奇数节点,3 台是最低可用规模。
| 主机 | IP 地址 | 节点角色 |
| K8S-node1 | 192.168.66.13 | Master、Node、etcd |
| K8S-node2 | 192.168.66.14 | Master、Node、etcd |
| K8S-node3 | 192.168.66.15 | Master、Node、etcd |
组件与要下的包
etcd 存集群状态和数据;kube-apiserver 是资源操作的唯一入口,负责认证、授权、访问控制、API 注册和服务发现;kube-controller-manager 维护集群状态(故障检测、自动扩容、滚动更新);kube-scheduler 按策略把 Pod 调到具体机器;kubelet 管容器生命周期、Volume 和网络(CNI);容器运行时默认 Docker;kube-proxy 给 Service 提供集群内的服务发现和负载均衡。插件另外装:kube-dns、Ingress Controller、Dashboard、Fluentd-elasticsearch 这一类。
需要的程序包:kubernetes-1.17.1(https://github.com/kubernetes/kubernetes/releases,Master 和 Node 都用 Server 端包,装 kubectl 用 client 包)、etcd-v3.3.18(https://github.com/etcd-io/etcd/releases)、flannel(https://github.com/coreos/flannel/releases,当时最新 0.11)、nginx-1.16.1、docker-19.03.5 静态包(https://download.docker.com/linux/static/stable/x86_64/)。




1.16 之后的版本要留意 API 变更:extensions/v1beta1 已经停用,NetworkPolicy 走 networking.k8s.io/v1、PodSecurityPolicy 走 policy/v1beta1、DaemonSet/Deployment/StatefulSet/ReplicaSet 走 apps/v1、Ingress 走 networking.k8s.io/v1beta1。旧的 YAML 不改 apiVersion 就是 apply 失败,Dashboard 1.10 这类旧版也装不上。转换可以用 kubectl convert -f <file> --output-version <group>/<version>,但它给的默认值未必理想。
系统准备
三台机器都要做,静态 IP、主机名、/etc/hosts、关 swap(/etc/fstab 里注释掉 swap 那行)、关 firewalld、SELinux 设为 disabled(后面 K8S 会生成大量 iptables 规则,不关就打架)、ntpd 定时同步、node1 到其余节点配 ssh 免密。内核参数:
[root@k8s-node1 ~]# cat > /etc/sysctl.d/kubernetes.conf <<EOF
net.bridge.bridge-nf-call-iptables=1
net.bridge.bridge-nf-call-ip6tables=1
net.ipv4.ip_forward=1
net.ipv4.tcp_tw_recycle=0
vm.swappiness=0
vm.overcommit_memory=1
vm.panic_on_oom=0
fs.inotify.max_user_instances=8192
fs.inotify.max_user_watches=1048576
fs.file-max=52706963
fs.nr_open=52706963
net.ipv6.conf.all.disable_ipv6=1
net.netfilter.nf_conntrack_max=2310720
EOF
#执行sysctl -p会出现如下报错:
sysctl: cannot stat /proc/sys/net/netfilter/nf_conntrack_max: 没有那个文件或目录
#解决方案:
[root@k8s-master1 ~]# cd /etc/modules-load.d/
[root@k8s-master1 ~]# vim netfilter.conf
nf_conntrack
[root@k8s-node1 ~]# for i in 192.168.66.{14,15};do scp /etc/modules-load.d/netfilter.conf root@$i:/etc/modules-load.d/;done
[root@k8s-node1 ~]# for i in 192.168.66.{13..15};do ssh root@$i "reboot";done
[root@k8s-node1 ~]# for i in 192.168.66.{13..15};do echo $i;ssh root@$i "modprobe br_netfilter;sysctl -p /etc/sysctl.d/kubernetes.conf";done
tcp_tw_recycle 必须关,它和 NAT 冲突会导致服务不通;IPv6 关掉是为了绕开一个 docker 的 bug。
内核升到 elrepo 的 kernel-lt,grub2-set-default 0 设置从新内核启动;然后在 /etc/default/grub 的 GRUB_CMDLINE_LINUX 一行加 numa=off,重新 grub2-mkconfig -o /boot/grub2/grub.cfg 后重启。


再建 docker 用户、用 docker info 检查各节点是否适合跑 Docker,最后把集群参数写进 /etc/profile。这一步是整套部署里最容易漏的:变量没生效,后面的模板文件替换和 kubeconfig 生成全是错的。
[root@k8s-node1 ~]# vim /etc/profile
#文件最后添加进去
#----------------------------K8S-----------------------------#
# 生成 EncryptionConfig 所需的加密 key
export ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
# Master集群各机器 IP 数组
export NODE_IPS=(192.168.66.13 192.168.66.14 192.168.66.15)
# Master集群各IP对应的 主机名数组
export NODE_NAMES=(k8s-node1 k8s-node2 k8s-node3)
# etcd 集群服务地址列表
export ETCD_ENDPOINTS="https://192.168.66.13:2379,https://192.168.66.14:2379,https://192.168.66.15:2379"
# etcd 集群间通信的 IP 和端口
export ETCD_NODES="k8s-node1=https://192.168.66.13:2380,k8s-node2=https://192.168.66.14:2380,k8s-node3=https://192.168.66.15:2380"
# kube-apiserver 的反向代理(kube-nginx)地址端口
export KUBE_APISERVER="https://127.0.0.1:8443"
# 节点间互联网络接口名称,根据实际服务器网卡名称配置
export IFACE="ens33"
# etcd 数据目录
export ETCD_DATA_DIR="/data/k8s/etcd/data"
# etcd WAL 目录,建议是 SSD 磁盘分区,或者和 ETCD_DATA_DIR 不同的磁盘分区
export ETCD_WAL_DIR="/data/k8s/etcd/wal"
# k8s 各组件数据目录
export K8S_DIR="/data/k8s/k8s"
# docker 数据目录
export DOCKER_DIR="/data/k8s/docker"
## 以下参数一般不需要修改
# TLS Bootstrapping 使用的 Token,可以使用命令 head -c 16 /dev/urandom | od -An -t x | tr -d ' ' 生成
BOOTSTRAP_TOKEN="b02ca253fcb632320613b00b6246a12c"
# 最好使用 当前未用的网段 来定义服务网段和 Pod 网段
# 服务网段,部署前路由不可达,部署后集群内路由可达(kube-proxy 保证)
SERVICE_CIDR="10.254.0.0/16"
# Pod 网段,建议 /16 段地址,部署前路由不可达,部署后集群内路由可达(flanneld 保证)
CLUSTER_CIDR="172.30.0.0/16"
# 服务端口范围 (NodePort Range)
export NODE_PORT_RANGE="30000-32767"
# flanneld 网络配置前缀
export FLANNEL_ETCD_PREFIX="/kubernetes/network"
# kubernetes 服务 IP (一般是 SERVICE_CIDR 中第一个IP)
export CLUSTER_KUBERNETES_SVC_IP="10.254.0.1"
# 集群 DNS 服务 IP (从 SERVICE_CIDR 中预分配)
export CLUSTER_DNS_SVC_IP="10.254.0.2"
# 集群 DNS 域名(末尾不带点号)
export CLUSTER_DNS_DOMAIN="cluster.local"
# 将二进制目录 /opt/k8s/bin 加到 PATH 中
export PATH=/opt/k8s/bin:$PATH
CA 证书、kubectl 与 etcd
装 cfssl,用 ca-config.json 定义签名场景(profile 和 usage:过期时间、服务端认证、客户端认证、加密),ca-csr.json 定义 CA 的 CN 和 host,cfssl gencert -initca 加 cfssljson -bare ca 出根证书,再把 CA、私钥、配置分发到所有节点的 /etc/kubernetes/cert。
[root@k8s-node1 work]# cfssl gencert -initca ca-csr.json | cfssljson -bare ca
2020/01/15 10:16:28 [INFO] generating a new CA key and certificate from CSR
2020/01/15 10:16:28 [INFO] generate received request
2020/01/15 10:16:28 [INFO] received CSR
2020/01/15 10:16:28 [INFO] generating key: rsa-2048
2020/01/15 10:16:29 [INFO] encoded CSR
2020/01/15 10:16:29 [INFO] signed certificate with serial number 6966905140966238303269956622170363884073553390
[root@k8s-node1 work]# ls ca*
ca-config.json ca.csr ca-csr.json ca-key.pem ca.pem
kubectl 需要最高权限的 admin 证书(CN=system:kube-apiserver 一类字段照模板来,hosts 留空,因为它只当 client 证书用),然后分四步生成 kubeconfig:设置集群参数、kubectl config set-credentials 设客户端认证、set-context 设上下文、use-context 设默认上下文。加 --embed-certs=true 才会把证书内容写进 kubeconfig,否则文件里存的是证书路径,换机器就失效。分发到各节点的 /root/.kube/config 后用 kubectl cluster-info 验证。





etcd 的 CSR 里 hosts 要列全三个节点 IP,否则会报 error "remote error: tls: bad certificate", ServerName ""。systemd unit 用模板文件加变量替换的方式为每个节点生成,systemctl start etcd 首次启动会卡一段时间——它在等其他成员加入,不是挂了。健康检查在每个节点上跑 etcdctl endpoint health,三个全 healthy 才算集群正常,再用 endpoint status 看当前 leader。
[root@k8s-node1 work]# cat > etcd.service.template <<EOF
[Unit]
Description=Etcd Server
After=network.target
After=network-online.target
Wants=network-online.target
Documentation=https://github.com/coreos
[Service]
Type=notify
WorkingDirectory=${ETCD_DATA_DIR}
ExecStart=/opt/k8s/bin/etcd \
--data-dir=${ETCD_DATA_DIR} \
--wal-dir=${ETCD_WAL_DIR} \
--name=##NODE_NAME## \
--cert-file=/etc/etcd/cert/etcd.pem \
--key-file=/etc/etcd/cert/etcd-key.pem \
--trusted-ca-file=/etc/kubernetes/cert/ca.pem \
--peer-cert-file=/etc/etcd/cert/etcd.pem \
--peer-key-file=/etc/etcd/cert/etcd-key.pem \
--peer-trusted-ca-file=/etc/kubernetes/cert/ca.pem \
--peer-client-cert-auth \
--client-cert-auth \
--listen-peer-urls=https://##NODE_IP##:2380 \
--initial-advertise-peer-urls=https://##NODE_IP##:2380 \
--listen-client-urls=https://##NODE_IP##:2379,http://127.0.0.1:2379 \
--advertise-client-urls=https://##NODE_IP##:2379 \
--initial-cluster-token=etcd-cluster-0 \
--initial-cluster=${ETCD_NODES} \
--initial-cluster-state=new \
--auto-compaction-mode=periodic \
--auto-compaction-retention=1 \
--max-request-bytes=33554432 \
--quota-backend-bytes=6442450944 \
--heartbeat-interval=250 \
--election-timeout=2000
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF

网络:flannel 与 apiserver 的四层代理
flanneld 从 etcd 读取网段分配,而 etcd 开了双向 x509 认证,所以 flanneld 也要单独签证书。Pod 网段信息只需写入一次,内容里的 SubnetFLANNELConfig 必须和后面 kube-apiserver 的 --cluster-cidr 一致。
[root@k8s-node1 work]# cd /opt/k8s/work
[root@k8s-node1 work]# etcdctl
--endpoints=${ETCD_ENDPOINTS}
--ca-file=/opt/k8s/work/ca.pem
--cert-file=/opt/k8s/work/flanneld.pem
--key-file=/opt/k8s/work/flanneld-key.pem
set ${FLANNEL_ETCD_PREFIX}/config '{"Network":"'${CLUSTER_CIDR}'", "SubnetLen": 21, "Backend": {"Type": "vxlan"}}'



检查各节点是否出现 flannel 接口(名字可能是 flannel0、flannel.0、flannel.1),然后跨节点 ping flannel 接口 IP 确认互通。
apiserver 高用可用 nginx 四层透明代理做:每起一个 nginx 进程,后端接三个 apiserver,nginx 做健康检查和负载均衡;kubelet、kube-proxy、controller-manager、scheduler 都通过本机 127.0.0.1 访问。编译参数只开 stream、关 http 和三个模块:
./configure --with-stream --without-http --prefix=/usr/local/nginx --without-http_uwsgi_module --without-http_scgi_module --without-http_fastcgi_module
make && make install
这样出来的二进制只依赖 libc 等核心库,不带 libz、libssl,各版本系统上都能跑。/usr/local/nginx/sbin/nginx 拷到各节点重命名为 kube-nginx,代理配置:
[root@k8s-node1 work]# cd /opt/k8s/work
[root@k8s-node1 work]# cat > kube-nginx.conf <<EOF
worker_processes 1;
events {
worker_connections 1024;
}
stream {
upstream backend {
hash $remote_addr consistent;
server 192.168.66.13:6443 max_fails=3 fail_timeout=30s;
server 192.168.66.14:6443 max_fails=3 fail_timeout=30s;
server 192.168.66.15:6443 max_fails=3 fail_timeout=30s;
}
server {
listen 127.0.0.1:8443;
proxy_connect_timeout 1s;
proxy_pass backend;
}
}
EOF

Master 三组件
kubernetes 证书里的 CN 是 kubernetes,hosts 列表要把 CLUSTER_KUBERNETES_SVC_IP(服务网段第一个 IP,可用 kubectl get svc 查)、各节点 IP 和 kubernetes.default.svc.cluster.local 这类域名都写进去,否则 Pod 通过服务名访问 apiserver 时证书校验过不了。另外要生成加密配置文件(EncryptionConfig),把 secret 在 etcd 里落盘的内容加密。




componentstatus 里 scheduler 常显示 Unhealthy,实际工作是正常的,这个 API 本来就不可靠。还要记一件事:不给 system:kube-controller-manager 授权访问 kubelet API,kubectl logs 会直接报权限错。controller-manager 和 scheduler 都走同样的四步(签证书、生成 kubeconfig、写 unit、启动),检查方式是 netstat -lnpt 看监听端口再加 curl 抓 metrics。controller-manager 监听 10252 收 https,scheduler 监听 10251 只收 http,还不支持 https。两者的 leader 都能从 endpoints 里看出来:
[root@k8s-node1 work]# kubectl create clusterrolebinding controller-manager:system:auth-delegator --user system:kube-controller-manager --clusterrole system:auth-delegator
clusterrolebinding.rbac.authorization.k8s.io/controller-manager:system:auth-delegator created









Worker:Docker、kubelet、kube-proxy
Docker 用静态二进制包,systemd unit 里关键是 EnvironmentFile=-/run/flannel/docker:flanneld 启动时把网络配置写进去,dockerd 启动前读它,docker0 网段才和集群一致。多个 EnvironmentFile 时这个文件必须放最后。还有个反复踩的坑——docker 1.13 起可能把 iptables FORWARD 链默认策略设成 DROP,导致 ping 不通其他节点的 Pod IP:
[root@k8s-node1 work]# iptables -P FORWARD ACCEPT
[root@k8s-node1 work]# vim /etc/rc.local
/sbin/iptables -P FORWARD ACCEPT
kubelet 用 TLS Bootstrapping 起步:先用 kubeadm 生成 token(kubeadm token list 查看、kubeadm token delete token_id 删除),为每个节点做一份 kubelet-bootstrap-${node_name}.kubeconfig,把 bootstrap 用户绑到 system:bootstrappers 组和 system:node-bootstrapper ClusterRole:
kubectl create clusterrolebinding kubelet-bootstrap --clusterrole=system:node-bootstrapper --group=system:bootstrappers
参数配置文件里 pause 镜像一定要用国内源(当时用阿里云的 pause 3.1),不然 Pod 起不来。unit 启动后 kubelet 会上报 CSR,等 1 到 10 分钟由 controller-manager 自动 approve(需要它带 --cluster-signing-key-file 参数);没自动批下来就 kubectl get csr 再 kubectl certificate approve <证书NAME> 手动批。自动签发的 CSR Group 是 system:bootstrappers:kubeadm:default-node-token,后面过期的 server 证书由另外的 ClusterRoleBinding 处理、Group 为 system:nodes。








kubelet 监听 10250。直接用 curl 访问 https://节点IP:10250/metrics 会被拒:token 和证书两条路都没过就返回 Unauthorized;用 admin 证书能过,因为 admin 被授予了所有 API 权限;用 kube-controller-manager 的证书则是 Forbidden,RBAC 里没有对应权限。测之前先建一个绑到 system:kubelet-api-admin 的 ServiceAccount,并确认 anonymous-auth 为 false,不允许匿名访问 10250。





kube-proxy 的配置文件由 --write-config-to 生成,里面写死 ipvs 模式;kubeconfig 同样四步。启动后检查监听端口和 metrics(10256 是 healthz),再用 ipvsadm -ln 看路由规则是否已经下发:
[root@k8s-node1 ~]# cd /opt/k8s/work
[root@k8s-node1 work]# cat <<EOF | tee kube-proxy-config.yaml.template
kind: KubeProxyConfiguration
apiVersion: kubeproxy.config.k8s.io/v1alpha1
clientConnection:
kubeconfig: "/etc/kubernetes/kube-proxy.kubeconfig"
bindAddress: ##NODE_IP##
clusterCIDR: ${CLUSTER_CIDR}
healthzBindAddress: ##NODE_IP##:10256
hostnameOverride: ##NODE_NAME##
metricsBindAddress: ##NODE_IP##:10249
mode: "ipvs"
EOF





验证与收尾
kubectl get nodes 三台都 Ready 之后,用一个 DaemonSet 加 NodePort Service 做端到端验证:
[root@k8s-node1 ~]# cd /opt/k8s/work
[root@k8s-node1 work]# cat > nginx-test.yml <<EOF
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
type: NodePort
selector:
app: nginx
ports:
- name: http
port: 80
targetPort: 80
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nginx
labels:
addonmanager.kubernetes.io/mode: Reconcile
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: my-nginx
image: nginx:1.16.1
ports:
- containerPort: 80
EOF
[root@k8s-node1 work]# kubectl apply -f nginx-test.yml
deployment.apps/nginx-deployment created

拿到三个 Pod IP(172.30.240.3、172.30.128.2、172.30.192.2),在所有节点上互相 ping,确认 flannel 打通;Pod 不运行就用 kubectl describe pod 找原因。Service 的 Cluster IP(10.254.254.189)、服务端口 80、NodePort 30064 都在 kubectl get svc 里,每台节点上 curl Cluster IP 应返回 nginx 欢迎页,浏览器访问 192.168.66.13:30064、.14、.15 三个地址都要通。
[root@k8s-node1 work]# for node_ip in ${NODE_IPS[@]}
do
echo ">>> ${node_ip}"
ssh ${node_ip} "ping -c 1 172.30.240.3"
ssh ${node_ip} "ping -c 1 172.30.128.2"
ssh ${node_ip} "ping -c 1 172.30.192.2"
done
[root@k8s-node1 work]# kubectl get svc |grep nginx
nginx NodePort 10.254.254.189 <none> 80:30064/TCP 61m
# Service Cluster IP:10.254.254.189
# 服务端口:80
# NodePort 端口:30064




重搭时的注意点
一、环境变量记得 source 才生效,echo ${PATH} 确认 /opt/k8s/bin 在里面;三台机器都要执行。二、所有 IP、网段、域名要和自己的实际环境对应,192.168.66.0/24、10.254.0.0/16、172.30.0.0/16 这三段不能和已有网络冲突,服务网段和 Pod 网段部署前路由不可达、部署后由 kube-proxy 和 flanneld 保证集群内可达。三、etcd 的 WAL 目录 ETCD_WAL_DIR 最好放独立 SSD 分区,和 ETCD_DATA_DIR 分开。四、kubelet 启动失败先查 swap 有没有真的关掉。五、CSR 长期 Pending 就去查 controller-manager 的签名参数和证书授权。六、这套部署里用到的镜像版本(pause 3.1、nginx-1.16.1、flannel 0.11、k8s 1.17)都是 2020 年前后的,生产环境请换成当前在维护的版本。



















暂无评论内容