K8S里跑得最稳的组件是etcd,出问题最要命的组件也是etcd。平时它安安静静地做整个集群的数据库,好像感觉不到存在,但一旦证书过期、节点磁盘损坏、或者需要扩容迁移,所有平时被掩盖的细节就全都冒出来了。我这些年处理过不少etcd的故障,最典型的就两类:节点要重建、证书要更换。这两件事都把K8S的证书体系牵扯得很深,稍有不慎就是整个控制面不可用。
这篇东西就是把我处理这两个场景的完整思路、实际命令和踩过的坑整理出来,从证书链路怎么组成的,到动手前要做什么检查,再到节点重建、证书更换的完整流程,最后附上常见报错排查对照。如果你正在维护K8S集群,或者准备接手一个kubeadm部署的集群,这篇能帮你省掉不少试错的时间。
1. 先理清楚:ETCD的证书到底有哪几类、都是谁在用
1.1 一张表看懂证书对应关系
大多数kubeadm部署的集群,etcd证书都存放在/etc/kubernetes/pki/etcd/目录下,另外还有两个跟etcd强相关的证书放在上一级/etc/kubernetes/pki/目录。这些文件名看着相似,但用途完全不同,我直接用一张表说明白。
| 证书文件 | 用途 | 谁是使用方 |
|---|---|---|
etcd/ca.crt、etcd/ca.key | etcd集群专属的根CA | 所有etcd证书的签发者,也是客户端校验etcd身份的信任锚点 |
etcd/server.crt、etcd/server.key | etcd对外提供服务的服务端证书 | 所有连接etcd的客户端(包括kube-apiserver、etcdctl)通过它验证服务端身份 |
etcd/peer.crt、etcd/peer.key | etcd节点间互相通信的peer证书 | 每个etcd成员之间进行TLS双向认证 |
etcd/healthcheck-client.crt、etcd/healthcheck-client.key | 本地健康检查用的客户端证书 | etcdctl、kubelet探测etcd健康状态时使用的客户端身份 |
apiserver-etcd-client.crt、apiserver-etcd-client.key | kube-apiserver访问etcd时使用的客户端证书 | kube-apiserver,这个文件在/etc/kubernetes/pki/下,不在etcd子目录里 |
这五个角色搞清楚之后,很多玄学报错就迎刃而解了。比如kubectl get nodes报连接超时,第一反应不是去看网络,而是确认apiserver拿着的apiserver-etcd-client.crt还是不是etcd信任的有效客户端证书。
1.2 证书之间的信任链是怎么建立的
etcd的证书体系本质就是一套TLS双向认证。CA是信任的根,server.crt解决“你连的确实是etcd”这个问题,peer.crt解决“我这个节点确实是集群成员”这个问题,apiserver-etcd-client.crt解决“我确实是apiserver,不是别的程序”这个问题。
为什么需要双向认证?因为etcd存着整个集群的状态,包括密钥、配置、资源定义。如果任何人都能向etcd发请求,等于把集群的管理权限直接暴露在外面。所以etcd要求客户端出示证书,它对端也要验证etcd的身份,两边互验,谁也别想冒充谁。
一个容易忽略的细节是:etcd节点之间的peer通信走的是2380端口,对外服务走的是2379。这两个端口的证书是分开的,peer.crt只用于2380端口,SAN里必须包含集群内各节点的peer地址;server.crt用于2379端口,SAN里必须包含客户端将要访问的地址,通常就是各节点IP和127.0.0.1、localhost。好多人换证书时只换一个文件,结果节点之间通信正常、但apiserver连不上,或者反过来,就是没弄清这个端口与证书的对应关系。
1.3 证书在哪些场景下会失效
证书失效不是只有“过期”这一种情况。按我遇到过的频率排序,大概是:
- 证书过期。kubeadm生成的etcd系列证书默认有效期是1年,CA是10年。所以最常见的场景是“叶子证书过期、CA还活着”。
- 证书里的SAN与节点实际地址不匹配。比如虚拟机迁移导致IP变了、主机名改了,但证书还是按旧地址签发的。
- 证书与私钥不匹配。恢复备份时只还原了
.crt忘了配套的.key,或者两个文件来自不同批次。 - CA不匹配。比如新节点加入集群时用了另一套CA签发的证书,导致对端不认。
- 系统时间偏差。证书校验依赖本机时钟,节点时钟偏差超过5分钟就可能出现“证书尚未生效”或“证书已过期”的报错,哪怕证书本身完全没问题。
搞清楚这些失效场景之后,你再看告警日志里的certificate has expired or is not yet valid,就会知道问题出在四个维度中的哪一个,而不是盲目地重签一张证书。
2. 动手前的体检:证书诊断与备份的正确姿势
2.1 三分钟查完全部证书有效期
动证书之前,先把集群所有相关证书的有效期扫一遍。我一般直接用openssl批量跑,不用额外装工具:
for f in /etc/kubernetes/pki/*.crt /etc/kubernetes/pki/etcd/*.crt; do echo "$f : $(openssl x509 -in "$f" -noout -enddate)" done输出里会清楚列出每一张证书的到期时间。如果发现某张证书快到期,或者已经到期,那基本就锁定问题了。
查SAN同样用openssl:
openssl x509 -in /etc/kubernetes/pki/etcd/server.crt -noout -text | grep -A1 "Subject Alternative Name"这一条命令能看出证书允许哪些IP和域名访问。实际操作中我发现,很多解决了半天最后发现是SAN漏了地址的情况,在这一步就能看出来。证书SAN是etcd证书排查里最值得优先确认的项目,因为报错日志里往往只显示“证书校验失败”,不会具体告诉你少了哪个地址。
2.2 证书与私钥匹配性检查
证书和私钥是否配对,用模组比对最快:
openssl x509 -in /etc/kubernetes/pki/etcd/server.crt -noout -modulus | md5sum openssl rsa -in /etc/kubernetes/pki/etcd/server.key -noout -modulus | md5sum两边输出一致,说明证书和私钥是配套的。不一致的话,TLS握手会在更早阶段失败,日志里通常是tls: private key does not match public key。这类问题在手工签发证书时特别容易发生,因为多个证书共用一套密钥文件,复制过程中容易张冠李戴。
2.3 动手前必须完成的备份与确认清单
证书操作没有后悔药,备份一定要做全。每次动etcd证书之前,我固定走这几步:
- 完整备份
/etc/kubernetes/pki/目录:
cp -a /etc/kubernetes/pki /etc/kubernetes/pki.bak.$(date +%Y%m%d%H%M)- 打一份etcd数据快照。注意快照只能从健康节点打,而且要提供正确的证书参数:
ETCDCTL_API=3 etcdctl \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \ --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \ snapshot save /root/etcd-backup.db这里有一个经验:快照文件一定要放在本地磁盘,不要直接放到被重建的那个节点上。我见过有人把快照存在etcd节点自身的/root下,结果节点磁盘损坏,快照也跟着没了。
记录当前etcd成员信息、各节点IP、证书文件列表。至少要把
etcdctl member list的输出存下来,后续重建节点时要对照使用。确认所有控制平面节点时钟同步。用
date看下每台机器时间,偏差过大的先chronyc makestep或ntpdate校正。
提示:证书操作中,任何一步做完都要验证一次集群状态,不要想着把所有节点改完再统一检查。etcd是强一致系统,一旦操作到一半发现异常,回滚的成本会随着节点数线性上升。
3. 实战记录一:ETCD节点重建的完整流程
3.1 哪些情况需要重建节点
节点重建不是说节点死了就要重建。大多数情况下,一个etcd节点宕机,只要在合理时间内恢复,集群依然能正常对外服务。真正需要走重建流程的场景主要有三类:
- 节点虚拟机彻底损坏,无法重启,只能重新创建一台机器。
- 数据目录(默认
/var/lib/etcd)损坏,里面的wal快照和member信息都没法用了。 - 证书和私钥丢失,且无法找回,必须用新身份加入集群。
这里要特别提醒一种错误做法:etcd节点挂了,直接把另一台健康节点的数据目录整个拷贝过去,然后启动etcd。这样做的后果非常严重,因为数据目录里包含成员ID和集群历史,用别人的数据启动,新节点会以别人的成员身份运行,集群里出现两个相同ID的节点,整个Raft协议直接陷入混乱。
3.2 从故障到恢复的全流程
以一个三节点etcd集群为例,假设三个节点分别为node-1、node-2、node-3,现在node-3彻底损坏,需要重建。
第一步:确认当前集群状态。在其他健康节点上进入etcd容器,或者使用宿主机安装的etcdctl,先查看成员列表:
etcdctl \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \ --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \ member list输出中能清楚看到哪个成员状态异常。三节点集群允许坏一个节点,但此时不能再动其他两个节点,否则quorum丢失,集群会进入只读状态。
第二步:从etcd集群中移除故障成员。记下故障成员的ID,然后执行:
etcdctl member remove <故障成员ID>移除之后,etcd成员列表只剩两个健康节点。这一步在操作逻辑上是先让集群“忘记”死掉的节点,后续新节点才能以全新身份加入,不会被提示成员已存在。
第三步:准备新节点环境。新机器的主机名尽量与旧节点保持一致,IP也尽量保持一致。如果IP无法保持一致,后面证书SAN和members的peer-urls都要跟着改,复杂度会高一些。新机器上需要提前安装好kubeadm、kubelet、kubectl,并确保镜像可用。
第四步:把控制平面证书同步到新节点。这里注意,新节点加入控制平面,必须拿到当前集群的CA才能签发后续新证书。最稳妥的方式是把健康控制平面节点上的/etc/kubernetes/pki/目录整个拷贝到新节点对应路径下,同时一并拷贝/etc/kubernetes/admin.conf。
第五步:加入控制平面。在新节点上执行:
kubeadm join <控制平面负载均衡地址>:6443 \ --token <token> \ --discovery-token-ca-cert-hash sha256:<hash> \ --control-plane如果网络环境里没有现成的token,可以在健康控制平面节点上用kubeadm token create --print-join-command重新生成。kubeadm执行join时,会检测到etcd集群中已有成员信息,自动把新节点以新成员身份加入etcd,然后拉起静态Pod。
提示:这里我默认的是“新节点可以复用原CA”的场景,这也是绝大多数内部集群的合理做法。如果你出于安全原因必须换CA,那就不是节点重建,而是证书轮换,走下一章的流程。
第六步:验证恢复结果。回到任意健康控制平面节点,确认新节点状态:
kubectl get nodes kubectl get --raw='/healthz?verbose' | grep etcd再用etcdctl检查所有成员健康:
etcdctl endpoint health --cluster全部返回healthy,重建就算完成了。
3.3 重建时最容易踩的三个坑
节点重建之所以容易翻车,我总结下来主要是三个坑。
第一个坑是peer证书的SAN漏写新地址。如果新节点的IP和主机名与旧节点不一致,那么就算kubeadm自动生成了新证书,其他节点验证peer证书时也会发现证书里的SAN不匹配,导致节点间握手失败。日志里会出现x509: certificate is valid for 192.168.1.13, not 192.168.1.99这类报错。解决办法就是重新签发peer证书,把新IP写进SAN,操作可以参考下一章的手工签发流程。
第二个坑是残留数据目录没清干净。新节点上如果/var/lib/etcd目录里还有旧数据(比如从故障机器恢复的镜像里带过来的),etcd启动时会误认为自己还是老成员,导致加入集群后反复报成员冲突。正确做法是把旧数据目录清空或改名,让etcd以全新状态启动。
第三个坑是peerURLs没有更新。成员列表里还写着旧地址的话,即使新节点启动成功,其他节点也连不上它。手工加入集群的情况下,需要先member update把peerURLs改成新地址。kubeadm方式一般会自动处理,但如果你用的是二进制部署或后来手工调整过集群,这一点要多留个心眼。
我遇到过最离谱的一次,是三个坑同时踩了一遍:新节点IP变了、数据目录没清、peerURLs没更新。那一次处理到凌晨三点,后来我把这三点写成了节点重建的固定检查项,就再没出过类似问题。
4. 实战记录二:ETCD集群更换证书的完整流程
4.1 先分清:续叶子证书和换CA是两码事
更换证书之前,必须先明确一个问题:你是打算复用现有CA,重新签发新的叶子证书?还是连根CA一起换掉?
大多数情况下,我们只需要做第一种,因为etcd的CA有效期是10年,而server、peer这些叶子证书默认只有1年。当叶子证书即将过期,执行kubeadm certs renew就能快速搞定,根本不需要动CA。但如果你遇到的是CA泄露、合规要求、或者整个集群是从别人手里接过来的、CA信任状况不清楚,那就得走第二种,也就是连根一起换。
两种方式的操作量和风险完全不同。第一种是可以滚动完成的,一次改一个节点,对集群影响可控。第二种涉及所有etcd成员、apiserver、kubelet以及其他通过etcd CA做校验的组件,链条更长,必须在维护窗口内操作,而且中途不能失手。
4.2 复用现有CA签发新证书的完整步骤
kubeadm提供的续期命令是所有步骤里最简单的。在控制平面节点上执行:
kubeadm certs renew etcd-server kubeadm certs renew etcd-peer kubeadm certs renew etcd-healthcheck-client kubeadm certs renew apiserver-etcd-clientrenew本质是基于/etc/kubernetes/pki/etcd/ca.crt和ca.key重新签发证书。命令执行完毕后,证书文件会直接覆盖到原路径。然后逐个节点重启etcd静态Pod,让新证书生效:
kubectl -n kube-system delete pod etcd-$(hostname)etcd是静态Pod,删除后kubelet会立即用新证书重新拉起。
注意:
kubeadm certs renew默认会保留旧证书的备份,但我不建议依赖这个自动备份。动手前我始终手动把整个pki目录打一个带时间戳的压缩包,放在集群之外的备份存储里。备份这步省掉,后面一旦出问题,回滚只能靠运气。
如果你不是kubeadm部署的集群,或者需要手工控制SAN列表,那就得用openssl手动签发。以签发peer证书为例,先创建一份openssl配置文件,这是整个签发过程里最关键的一步:
[ req ] distinguished_name = dn req_extensions = v3_req prompt = no [ dn ] CN = etcd-node-3 [ v3_req ] basicConstraints = CA:FALSE keyUsage = digitalSignature, keyEncipherment extendedKeyUsage = serverAuth, clientAuth subjectAltName = @alt_names [ alt_names ] DNS.1 = node-3 DNS.2 = localhost IP.1 = 192.168.1.13 IP.2 = 127.0.0.1然后基于这份配置生成证书:
openssl req -new -key /etc/kubernetes/pki/etcd/peer.key -out peer.csr -config peer-openssl.cnf openssl x509 -req -in peer.csr -CA /etc/kubernetes/pki/etcd/ca.crt -CAkey /etc/kubernetes/pki/etcd/ca.key -CAcreateserial -out /etc/kubernetes/pki/etcd/peer.crt -days 365 -extensions v3_req -extfile peer-openssl.cnfserver证书的签发流程完全一样,只是CN和SAN要按服务端需求来。server证书的SAN里必须包含etcd集群所有节点的地址,还要有127.0.0.1和localhost,因为kube-apiserver默认是通过https://127.0.0.1:2379访问本地etcd的,如果漏掉本地回环地址,apiserver会直接连接失败。
handcheck-client和apiserver-etcd-client这两个客户端证书,签发时的关键点在于extendedKeyUsage必须包含clientAuth,CN要能对应上etcd侧配置的client-cert-allowed-cn规则。如果CN写错,etcd会拒绝握手。
4.3 连根一起换:CA轮换的注意事项
当需要换CA时,事情就不会那么温和了。换CA的本质是:所有信任关系全部作废重建。需要更新的文件包括:
- 所有etcd节点的
server.crt、peer.crt、healthcheck-client.crt - apiserver使用的
apiserver-etcd-client.crt - apiserver静态Pod清单里的
--etcd-cafile指向的文件,也就是etcd/ca.crt
CA轮换我推荐的做法是先用新CA签发一套全新的证书,再把证书分发到各节点,然后按节点依次切换。切换顺序一定是挨个来,绝对不能同时重启两个以上etcd节点。三节点集群在不丢quorum的前提下,最多只能容忍一次只挂一个节点,如果你同时重启两个节点,剩下的单节点无法构成多数派,整个集群立刻变成只读,apiserver也会跟着爆超时。
每一步切换之后,都要反复确认etcdctl endpoint health --cluster全部健康,再继续下一步。
4.4 别忘了apiserver这个“客户端”
很多人在换完etcd自己的证书后,发现apiserver仍然狂报错,原因就是忘了更新apiserver作为etcd客户端使用的证书。apiserver和etcd之间的认证关系非常直接:apiserver用apiserver-etcd-client.crt证明身份,同时用etcd/ca.crt校验连接的确实是etcd。所以换CA时,这两个文件必须同步更新,否则apiserver会拿着旧客户端证书去连新CA签发的etcd服务端证书,日志里全是client certificate is not trusted。
替换完apiserver侧证书后,同样需要重启apiserver静态Pod:
kubectl -n kube-system delete pod kube-apiserver-$(hostname)重启之后,查看apiserver日志确认已经没有etcd连接相关报错:
kubectl -n kube-system logs kube-apiserver-$(hostname) | grep -i etcd这一步在CA轮换里很容易被漏掉,因为在操作层面它不在/etc/kubernetes/pki/etcd/目录下,而是在上一级pki目录里,不专门提一句,很多人根本想不起来。
5. 常见问题与排查技巧实录
5.1 证书类错误日志速查表
我把实际运维中遇到过的高频报错整理成一张速查表,按日志关键字检索,可以快速定位问题方向。
| 日志关键字 | 可能原因 | 排查动作 |
|---|---|---|
x509: certificate signed by unknown authority | apiserver的etcd-cafile指向的CA与etcd实际CA不一致 | 对比两个ca.crt的md5 |
x509: certificate is valid for ..., not ... | 证书SAN缺少对端访问的地址 | openssl x509 -noout -text查看SAN列表 |
client certificate is not trusted | apiserver的etcd客户端证书CN或用途不匹配 | 检查apiserver-etcd-client.crt是否由etcd CA签发 |
certificate has expired or is not yet valid | 证书过期,或节点时钟偏差 | 扫有效期,确认各节点时间同步 |
etcdserver: request timed out | quorum丢失、网络不通或证书握手失败 | 先查member list,再看每个endpoint健康状态 |
failed to connect to member | peerURLs错误、peer证书SAN不匹配 | etcdctl member list对比地址与证书SAN |
这张表看起来简单,但很多“灵异现象”最后都能落到上面某一行。尤其是etcdserver: request timed out,它背后的网络排查链条很长,但第一排查项永远是确认etcd节点之间的TLS握手是否正常,而不是直接去ping IP。
5.2 恢复现场:回滚方案与快照恢复
如果更换证书过程中发现问题,第一反应应该是回滚,而不是继续修。回滚的逻辑很简单:把之前备份的旧证书文件原路径放回去,然后重启相关的etcd或apiserver静态Pod。只要旧证书文件还在,这个操作几分钟内就能完成。
但如果连数据都出了问题,就需要从快照恢复。恢复快照时有个重要原则:所有节点必须从同一个快照出发,不能只恢复单节点。
ETCDCTL_API=3 etcdctl snapshot restore /root/etcd-backup.db \ --name=node-1 \ --data-dir=/var/lib/etcd-new \ --initial-cluster=node-1=https://192.168.1.11:2380,node-2=https://192.168.1.12:2380,node-3=https://192.168.1.13:2380 \ --initial-cluster-token=etcd-restore每个节点上都要执行一次restore命令,--name和--data-dir改成对应的节点名。恢复出来的数据目录是全新的,成员ID全部重新生成,因此集群内不会与旧数据冲突。恢复完成后,把/var/lib/etcd-new改名为/var/lib/etcd,再启动etcd。
这里的--initial-cluster-token必须设置成一个新值,作用是把恢复后的集群与历史集群隔离开,防止节点启动时误连旧集群。很多人忽略这个参数,导致恢复后的节点一直尝试连接快照之外的旧成员。
5.3 长期维护建议:给证书做一个有效期监控
证书过期这件事,最好的解决方案就是别让它发生。我的做法很粗暴:写一个简单的有效期巡检脚本,放到cron里,每天跑一次,低于30天就告警。
#!/bin/bash for cert in /etc/kubernetes/pki/*.crt /etc/kubernetes/pki/etcd/*.crt; do exp=$(openssl x509 -in "$cert" -noout -enddate | cut -d= -f2) left=$(( ($(date -d "$exp" +%s) - $(date +%s)) / 86400 )) echo "$cert : ${left} days left" done脚本本身没有技术含量,但能让你提前一个月知道证书要过期。etcd证书的续期操作在维护窗口内做和故障紧急处理做,完全是两种体验。
另外,每次证书轮换后,我都会把SAN列表和证书有效期存一份到独立的运维文档里。下次再有人问“这证书还能用多久”,直接翻文档比上机器查要快得多,而且能追溯每次轮换的具体内容。
个人经验的一点总结
我做etcd证书相关操作有一个固定习惯:动手前先在终端里把每张证书的SAN和有效期打印一遍,保存到一个临时文件里;操作完成后再打印一遍,做一次diff。整个过程看起来多花了十分钟,但很多时候就是这十分钟帮你避免了“换完之后发现某个地址被漏掉”的返工。
还有一点,etcd证书问题永远先看日志,再看配置,最后才考虑重签。日志里会明确告诉你TLS握手失败发生在哪个环节,按着kube-apiserver和etcd两边的日志对照排查,绝大多数问题都能定位到具体的证书文件上。证书这东西,最忌讳的就是凭感觉操作,一步一验证,比什么技巧都管用。