Skip to content

Kubernetes

18 posts with the tag “Kubernetes”

在 K8s/K3s 中为特定 Pod 分配独立物理 IP (Multus + Macvlan + VLAN)

在构建 HomeLab 或企业内网时,我们经常会将服务部署在 K8s/K3s 集群中。默认情况下,集群内的出站流量会经过 CNI(如 Flannel 或 Cilium)的 SNAT 转换,源 IP 会变成宿主机的 Node IP。

但在某些场景下(例如部署 qBittorrent 等 P2P 下载工具、需要被独立监控的服务,或者需要绕过网关透明代理的流量),我们需要让 Pod 拥有一个真实的局域网独立 IP,并且可能还需要将其划分到**特定的 VLAN(如 VLAN 10)**中。

本文将介绍如何使用 Multus CNI 配合 Macvlan,为特定的 Pod “插上一根直通物理局域网的网线”,并配置 OPNsense 网关实现流量直连。

本方案完美兼容硬核 HomeLab 常用的 Cilium CNI + kube-vip 环境。各组件分工明确,互不冲突:

  • Cilium (主 CNI & Service LB):负责集群内 eBPF 流量管理与 Service LB IP 分配,为 Pod 提供基础的 eth0 网卡。
  • kube-vip:负责控制平面 API Server 的 VIP 漂移与高可用。
  • Multus + Macvlan (旁路直通):作为一个旁路插件,专门为指定的 Pod 塞入第二块物理网卡 net1

⚠️ 核心避坑:IP 地址池必须严格隔离! 必须确保 Macvlan 使用的静态 IP(本文以 10.0.10.101 为例),与 Cilium LB IPAM 池、kube-vip 的地址、以及 OPNsense 的 DHCP 池完全错开,避免产生 IP 冲突。


Multus 作为一个“元 CNI”,允许 Pod 挂载多块网卡。如果你的集群尚未安装,可以通过官方提供的 DaemonSet 一键安装(Thick 模式兼容性最好):

Terminal window
kubectl apply -f https://raw.githubusercontent.com/k8snetworkplumbingwg/multus-cni/master/deployments/multus-daemonset-thick.yml
kubectl set resources daemonset kube-multus-ds -n kube-system -c kube-multus --limits=memory=512Mi --requests=memory=100Mi

等待所有 kube-system 命名空间下的 multus Pod 处于 Running 状态即可。

步骤二:创建网络附件定义 (NetworkAttachmentDefinition)

Section titled “步骤二:创建网络附件定义 (NetworkAttachmentDefinition)”

我们需要创建一个 CRD 资源,告诉 K8s 如何划分这个直通的局域网。

创建一个名为 macvlan-network.yaml 的文件。注意:该资源必须与目标 Pod 处于同一个 Namespace。

💡 关于 VLAN 10 的特殊说明:

  • 情况 A(推荐 PVE 用户): 如果你在 PVE 虚拟机的硬件设置中,为该网卡配置了 VLAN Tag: 10,那么 K3s 系统内部的物理网卡(如 eth0ens18)已经是解包后的 VLAN 10 网络了。下方配置中的 master 直接填 eth0 即可。
  • 情况 B(Trunk 模式): 如果 PVE 传递的是 Trunk 口,你需要在 K3s 宿主机系统层面(如 Netplan 或 NetworkManager)先创建一个 VLAN 子接口 eth0.10,然后下方的 master 必须填入 eth0.10
macvlan-network.yaml
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
name: macvlan-direct
namespace: default # 必须替换为你的应用所在的 namespace
spec:
# ⚠️ 核心提醒:下方 config 内的字符串会被解析为纯 JSON,绝对不能包含 '#' 注释。
# 所有的关键备注已经为你提取到此处,请根据这些提示修改下方 JSON 对应的值:
#
# 1. master: 根据上方 VLAN 说明,替换为 eth0 或 eth0.10
# 2. subnet: 10.0.10.0/24 (VLAN 10 的真实网段)
# 3. rangeStart: 10.0.10.101 (为 Pod 分配的静态独立 IP)
# 4. rangeEnd: 10.0.10.101 (与 rangeStart 保持一致)
# 5. gateway: 10.0.10.1 (VLAN 10 的网关)
# 6. routes: 必须添加 0.0.0.0/0 的默认路由,否则无法访问外网 (Tracker 会报错)
config: |
{
"cniVersion": "0.3.1",
"type": "macvlan",
"master": "eth0",
"mode": "bridge",
"ipam": {
"type": "host-local",
"subnet": "10.0.10.0/24",
"rangeStart": "10.0.10.101",
"rangeEnd": "10.0.10.101",
"gateway": "10.0.10.1",
"routes": [
{ "dst": "0.0.0.0/0", "gw": "10.0.10.1" }
]
}
}

应用此配置:kubectl apply -f macvlan-network.yaml

步骤三:修改应用的 Deployment 配置

Section titled “步骤三:修改应用的 Deployment 配置”

在应用的 template.metadata.annotations 中声明使用刚刚创建的网络。同时,为了避免 DNS 流量黑洞,我们需要让该 Pod 绕过集群内部的 CoreDNS,直接使用公网 DNS。

以部署 qBittorrent 为例:

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: qbittorrent
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: qbittorrent
template:
metadata:
labels:
app: qbittorrent
annotations:
# 绑定前面创建的 Macvlan 网络
k8s.v1.cni.cncf.io/networks: macvlan-direct
spec:
# 跳过 K8s CoreDNS,直接使用公网 DNS 解析 Tracker
dnsPolicy: "None"
dnsConfig:
nameservers:
- 223.5.5.5
- 114.114.114.114
# 必须固定节点,防止漂移导致物理网卡名称不匹配
nodeSelector:
kubernetes.io/hostname: k3s-worker-02
containers:
- name: qbittorrent
image: lscr.io/linuxserver/qbittorrent:latest
# ... 其他配置保持不变

重新部署后,进入 Pod 内部执行 ip a,你会发现它拥有了两张网卡:eth0 (集群内部 IP) 和 net1 (直连 VLAN 10 的 10.0.10.101)。

关键一步: 必须在应用的内部设置(如 qBittorrent 的高级设置 -> 网络接口)中,强制将监听网络接口绑定为 net1,确保流量不走 Cilium 的默认路由,而是真正从 Macvlan 发出。

步骤四:在 OPNsense 中设置 NAT 豁免 (Do not NAT)

Section titled “步骤四:在 OPNsense 中设置 NAT 豁免 (Do not NAT)”

当 qB 绑定 net1 向外发送数据时,流量会到达 OPNsense 网关。由于 OPNsense 默认会对出站流量进行 NAT 伪装,我们需要对这个特定的 IP 进行“豁免”,让最外层的设备(如 OpenClash)能看到它的真实源 IP。

  1. 进入设置菜单: 登录 OPNsense,导航至 防火墙 (Firewall) -> NAT -> 出站 (Outbound)
  2. 修改模式: 将出站 NAT 模式修改为 混合出站 NAT 规则生成 (Hybrid outbound NAT rule generation),点击保存。
  3. 添加豁免规则: 点击右上角的 + 号添加一条新规则。
  4. 关键配置项:
    • 不进行 NAT (Do not NAT): 必须勾选页面最上方这个复选框。
    • 接口 (Interface): 选择流量离开 OPNsense 去往上层网络(如光猫或旁路由)的接口,通常是 WAN
    • TCP/IP 版本: IPv4
    • 协议 (Protocol): any
    • 源地址 (Source address): 下拉选择 单主机或网络 (Single host or Network),并输入分配给 Pod 的独立 IP 10.0.10.101,掩码选择 /32
    • 转换/目标 (Translation / target): 保持默认(勾选了不进行 NAT 后,此项会失效)。
  5. 调整规则优先级: 保存规则后,回到列表页。将这条新创建的豁免规则拖动到所有规则的最顶部(确保它在常规的子网 NAT 规则之前生效),然后点击 应用更改 (Apply Changes)

步骤五:配置透明代理直连 (OpenClash)

Section titled “步骤五:配置透明代理直连 (OpenClash)”

完成 NAT 豁免后,外部网络(包括旁路代理)就能直接识别到 10.0.10.101 这个源 IP 了。

进入你的代理软件(如 OpenClash),在 访问控制 (Access Control) -> 不走代理的局域网设备 (Not Proxy LAN IPs) 中,将 10.0.10.101 加入列表。至此,你的 P2P 下载流量将完美绕过集群的网络虚拟化层和全局科学代理。

步骤六:访问方式变更与联动服务更新 (*arr)

Section titled “步骤六:访问方式变更与联动服务更新 (*arr)”

当你为 Pod 加上了默认物理路由并绑定了网卡后,它已经彻底变成了一台插在局域网上的“实体机”。这会带来两个必须处理的变更:

  1. 集群 Ingress / 域名访问失效: 由于流量现在是从集群的 eth0 进,却从直连的 net1 出,这会导致严重的“异步路由”(Asymmetric Routing),使得原有的 Nginx/Traefik Ingress 域名访问或 NodePort 访问不断转圈超时。
    • 解决方案: 放弃域名和 NodePort 转发,以后请直接在浏览器输入分配的独立 IP 与端口(如 http://10.0.10.101:8080)来访问后台 WebUI。
  2. 更新 Radarr / Sonarr 下载客户端: 如果你使用了 Radarr、Sonarr 或 Prowlarr 等自动化追剧工具,由于 qBittorrent 的访问入口已经改变,你必须前往这些工具的 设置 -> 下载客户端 (Download Clients) 中,将 qBittorrent 的主机地址更新为新的独立 IP(10.0.10.101)。如果不更新,这些联动组件将无法把种子推送到下载器中。

如果在配置过程中遇到 Pod 无法获取独立 IP 的情况,通常是由以下三个核心问题导致的:

如果你的 K3s 节点是运行在 Proxmox VE (PVE) 等虚拟化平台上的虚拟机,请务必注意以下两点限制:

  • 混杂模式与 MAC 地址过滤: 进入 PVE 的虚拟机设置,找到 硬件 (Hardware) -> 网络设备 (Network Device),双击打开编辑,关闭“防火墙 (Firewall)”选项。如果开启该选项,PVE 会拦截 Macvlan 自动生成的非原生 MAC 地址,导致 Pod 无法联网。
  • VLAN 隔离问题: 如果你在 Macvlan 层面使用的是子接口(如 eth0.10),请确保 PVE 分配给该虚拟机的网桥(Bridge)开启了 VLAN 感知 (VLAN aware),否则宿主机将会直接丢弃带有 VLAN Tag 的数据包。

2. Cilium 独占模式导致 Multus 失效

Section titled “2. Cilium 独占模式导致 Multus 失效”

如果你发现配置完全正确,但 Pod 依然只分配了 Cilium 的默认内部 IP,请登录宿主机检查 /etc/cni/net.d/ 目录。如果你看到 00-multus.conf 被系统自动重命名为了 00-multus.conf.cilium_bak,这说明 Cilium 开启了“独占模式”,强行禁用了 Multus。

解决办法: 关闭 Cilium 的独占模式并重启相关组件抢回控制权:

Terminal window
# 1. 修改 ConfigMap,关闭独占模式
kubectl patch configmap cilium-config -n kube-system --type merge -p '{"data":{"cni-exclusive":"false"}}'
# 2. 重启 Cilium 代理以应用新配置
kubectl rollout restart daemonset cilium -n kube-system
# 3. 等待 Cilium 启动后,重启 Multus 使其重新生成 00-multus.conf
kubectl rollout restart daemonset kube-multus-ds -n kube-system

3. K3s 节点缺失官方 CNI 基础插件

Section titled “3. K3s 节点缺失官方 CNI 基础插件”

K3s 是一个极致精简的轻量级发行版,特别是配合 Cilium 使用时,宿主机的 /opt/cni/bin 目录下可能并没有预装 macvlan 等基础官方插件。此时 Multus 虽能正常工作,但在为 Pod 创建网络沙盒时会抛出如下报错: failed to find plugin "macvlan" in path [/opt/cni/bin]

解决办法: SSH 登录到运行该 Pod 的 K3s 宿主机节点,手动下载并解压官方的标准 CNI 插件包:

Terminal window
# 确保目录存在
sudo mkdir -p /opt/cni/bin
# 下载官方 CNI 插件包 (此处以稳定的 v1.4.1 为例)
wget https://github.com/containernetworking/plugins/releases/download/v1.4.1/cni-plugins-linux-amd64-v1.4.1.tgz
# 将压缩包解压到 /opt/cni/bin 目录下
sudo tar -C /opt/cni/bin -xzf cni-plugins-linux-amd64-v1.4.1.tgz
# 重建失败的 Pod 即可正常挂载

K8s 存储踩坑记:强拆 Longhorn '僵尸 Pod' 钉子户

在折腾 K3s + Longhorn 搭建我的赛博影音堡垒时,我遇到了一个极其让人血压升高的玄学 Bug。

整个流水线突然停工,应用 Pod 疯狂报错无法启动,而罪魁祸首,居然是一个在系统里“死活退不出来”的僵尸进程。

一切起因于我修改了媒体应用(Radarr/Sonarr)的配置文件并重新部署。新 Pod 迟迟无法启动,查看事件日志,赫然飘着那句经典的存储死锁报错:

Multi-Attach error for volume ... Volume is already exclusively attached to one node and can't be attached to another

意思是:存储卷正被旧节点死死锁着,新 Pod 拿不到权限。

顺藤摸瓜,我发现底层负责管理挂载的 longhorn-csi-plugin Pod 一直卡在 Terminating(正在终止)状态,时长超过了 20 分钟!查看它的详细日志,看到了这句致命的超时宣告:

error killing pod: failed to "KillContainer" for "longhorn-liveness-probe" ... rpc error: code = DeadlineExceeded desc = context deadline exceeded

🧠 赛博验尸报告:为什么它“杀不死”?

Section titled “🧠 赛博验尸报告:为什么它“杀不死”?”

表面上看是 Kubernetes 出了 Bug,但深入探究,这其实是 Linux 内核底层的自我保护机制导致的。

  1. I/O 阻塞:由于之前尝试跨网段挂载 NFS 导致了网络超时,底层的存储进程卡在了“等待磁盘响应”的状态。
  2. 内核级锁死 (D-state):在 Linux 中,当进程卡在底层 I/O 等待时,内核会将其标记为 不可中断睡眠 (Uninterruptible Sleep, D状态)。在这个状态下,进程会无视任何外部信号(包括 kill -9)。
  3. Kubelet 懵逼:Kubernetes 的包工头 Kubelet 给容器引擎下达了“拔管”指令,但底层容器因为 D 状态根本不鸟它。Kubelet 苦等 2 分钟后无奈宣布超时(DeadlineExceeded),于是这个 Pod 就成了一个永远卡在 Terminating 的“钉子户”,并且牢牢霸占着 Longhorn 的存储锁。

对付这种级别的钉子户,常规的清理已经没用了。必须采取暴力强拆手段。

第一板斧:K8s 逻辑强抹除(本次生效的绝招)

Section titled “第一板斧:K8s 逻辑强抹除(本次生效的绝招)”

既然正常的优雅退出走不通,那就直接在 K8s 的记录本上把它强行划掉,不给任何宽限时间。

执行以下命令强杀卡死的 Pod:

Terminal window
kubectl delete pod <卡住的Pod名称> -n longhorn-system --force --grace-period=0

效果:命令敲下的瞬间,Pod 从列表中消失,Longhorn 控制平面终于意识到锁可以释放,随后新 Pod 瞬间挂载存储卷成功,业务恢复!

第二板斧:重启底层容器引擎(备用方案)

Section titled “第二板斧:重启底层容器引擎(备用方案)”

如果在执行完第一步后,kubectl get pods 里确实没它了,但应用依然报 Multi-Attach error,说明底层 Linux 进程依然在霸占资源。此时需要重启 K3s 的 agent 服务来重置容器状态:

Terminal window
# 登录出问题的 K3s 工作节点执行
sudo systemctl restart k3s-agent
# 如果是主节点则执行 restart k3s

第三板斧:物理重启(终极毁灭)

Section titled “第三板斧:物理重启(终极毁灭)”

如果前两招都打完,存储依然处于死锁状态,说明 Linux 内核的 I/O 队列已经彻底崩溃。别犹豫,直接 sudo reboot 重启该节点所在的物理机或虚拟机,这是清理 D 状态进程最彻底的方案。

这次踩坑不仅让我深入理解了 Kubernetes 和底层操作系统的联动关系,也总结出了一条铁律:

在 HomeLab 环境中,对于独占存储(ReadWriteOnce)的单副本有状态应用(如 qBittorrent、数据库等),在 deployment.yaml 中务必将更新策略设置为重建!

spec:
replicas: 1
strategy:
type: Recreate # 强制先杀旧,再启新,绝不抢锁

遇到 Terminating 不要慌,找准病根,果断拔管!

🚨 赛博排障实录:CNPG 密码“脑裂”与内核级破门指南

⚠️ 事故现场:完美的图纸,打不开的大门

Section titled “⚠️ 事故现场:完美的图纸,打不开的大门”

在部署 pgAdmin 接入 CloudNativePG (CNPG) 时,即便你严格遵守了 GitOps 的每一行配置,提取了最新的 K8s Secret 密码,依然可能遇到那个令人抓狂的报错:

pgAdmin 4 - Connection Error
FATAL: password authentication failed for user "postgres"

这种“钥匙(Secret)打不开自家大门(Database)”的现象,在 Operator 模式下被称为 “状态撕裂” (State Desync),俗称 “脑裂”


🧠 深度复盘:为什么会被“天坑”算计?

Section titled “🧠 深度复盘:为什么会被“天坑”算计?”

这本质上是 Kubernetes 的声明式状态数据库的物理持久化状态 发生了脱节。

CNPG 管家只在集群 第一次初始化 (Bootstrap) 的那一瞬间,会生成随机密码并同时写入 K8s Secret 和数据库底层引擎。

2. K8s 与物理存储 (Longhorn) 的脱节

Section titled “2. K8s 与物理存储 (Longhorn) 的脱节”

如果你曾删除过 CNPG 实例但保留了 Longhorn 数据卷,当你再次拉起集群时:

  • K8s 视角:这是一个新任务,它会生成一个全新的 Secret(新钥匙)。
  • Postgres 视角:挂载旧硬盘后,它依然守着第一次建库时的旧密码(老锁芯)。
  • 结果:外面显示的密码是 A,但门里认的密码是 B。

🔨 赛博暴力破门方案:潜入内核强行修正

Section titled “🔨 赛博暴力破门方案:潜入内核强行修正”

既然外面的钥匙失效了,作为赛博堡垒的造物主,我们不再纠结于 Secret,直接利用 Socket 直连 潜入数据库心脏,从内部强制重置密码。

利用 kubectl exec 绕过网络认证,以本地超级用户身份进入数据库 Pod:

Terminal window
# 进入主节点终端并直连 psql
kubectl exec -it -n database homelab-db-cluster-1 -- psql

进入 postgres=# 提示符后,执行 SQL 语句进行“物理修正”:

-- 强行将 postgres 用户的锁芯更换为你预设的密码
ALTER USER postgres WITH PASSWORD '你的新密码';
-- 退出内核
\q

👑 总结:首席承包商的终极底牌

Section titled “👑 总结:首席承包商的终极底牌”

这次排障告诉我们一个真理:在云原生时代,GitOps 是流程,但 kubectl 是真理。

不管外面的声明式配置如何演变,只要你能以系统造物主的身份进行“内核级干预”,你就永远拥有整座赛博堡垒的最终解释权。

  • 重建集群时:若要彻底重置密码,必须连同 Longhorn 的 PVC 一起清理。
  • 排障优先级:当应用层认证失败时,优先检查物理存储是否包含陈旧状态。

🗺️ 脑裂排障逻辑图 (Troubleshooting Flow)

Section titled “🗺️ 脑裂排障逻辑图 (Troubleshooting Flow)”

K8s 焦土政策:网关与证书系统的终极卸载指南

这是一份针对 Kubernetes 集群的**“彻彻底底、连根拔起”**清理方案。

当你的网关和证书系统因为各种改动搅得一团糟,且面临诸如 ArgoCD 陷入死锁、命名空间卡在 Terminating 无法删除等极其顽固的底层状态错误时,**“焦土政策(物理核平)”**绝对是最有效、最干净的排障手段。

严格按照以下顺序执行。这套连招能保证把系统里的残留基因拔得干干净净,绝不影响下一次重装。


☢️ 第一阶段:斩断源头 (切断 ArgoCD 重建链)

Section titled “☢️ 第一阶段:斩断源头 (切断 ArgoCD 重建链)”

在使用 GitOps (如 ArgoCD) 时,决不能直接去删底层资源,否则 ArgoCD 会发现“建筑没了”,并立刻把它们重新拉起来。我们必须先切断图纸的下发。

Terminal window
kubectl delete application cert-manager traefik -n argocd

2. 遭遇死锁?强行剥夺 ArgoCD 的遗愿清单

Section titled “2. 遭遇死锁?强行剥夺 ArgoCD 的遗愿清单”

如果上面的命令卡住转圈(报错 metadata.finalizers: Forbidden),说明 ArgoCD 本身陷入了死结。立刻打开新终端,执行强制“拔管”:

Terminal window
kubectl patch application cert-manager -n argocd -p '{"metadata": {"finalizers": null}}' --type merge
kubectl patch application traefik -n argocd -p '{"metadata": {"finalizers": null}}' --type merge

执行后,ArgoCD 面板上的应用会瞬间消失,系统不再自动重建。


💣 第二阶段:深度扫荡 (清理 Webhooks 与 全局 CRDs)

Section titled “💣 第二阶段:深度扫荡 (清理 Webhooks 与 全局 CRDs)”

这是 99% 的人重装失败的根源! 命名空间好删,但全局注册的蓝图规范(CRD)和拦截器(Webhook)如果不清空,下次安装必定会报出各种版本冲突。

Cert-manager 会向 K8s API Server 注入拦截器,必须优先干掉,否则它会拦截你接下来的所有安装请求:

Terminal window
kubectl delete validatingwebhookconfiguration cert-manager-webhook --ignore-not-found
kubectl delete mutatingwebhookconfiguration cert-manager-webhook --ignore-not-found
Terminal window
# 删光 Traefik 和 Cert-manager 的残留基因
kubectl get crd -o name | grep -E 'traefik|cert-manager' | xargs kubectl delete

3. 扫荡残留的“全局权限” (RBAC)

Section titled “3. 扫荡残留的“全局权限” (RBAC)”

这一步经常被忽视,导致重装时报 Ownership/Adoption 错误:

Terminal window
# 清理集群级别的角色与绑定
kubectl get clusterrole,clusterrolebinding -o name | grep -E 'cert-manager|traefik' | xargs kubectl delete
# 清理隐藏在 kube-system 核心区的领导者选举权限
kubectl get role,rolebinding -o name -n kube-system | grep cert-manager | xargs -I {} kubectl delete {} -n kube-system

🧨 第三阶段:核平街区 (强制清理 Namespace 内部残留)

Section titled “🧨 第三阶段:核平街区 (强制清理 Namespace 内部残留)”

如果你发现 Namespace 删不掉,或者里面总剩下一堆 Role, Service, ServiceAccount,那是它们身上带着 Finalizers(保护锁)

在删除 Namespace 之前,先手动清除这些顽固住户的遗愿清单:

Terminal window
# 定义目标命名空间
TARGET_NS=("cert-manager" "traefik")
for ns in "${TARGET_NS[@]}"; do
echo "正在强制碎锁 $ns 中的局部资源..."
# 批量清除 Role, RoleBinding, ServiceAccount, Service 的保护锁
kubectl get rolebinding,role,serviceaccount,service -n $ns -o name | xargs -I {} kubectl patch {} -n $ns -p '{"metadata":{"finalizers":null}}' --type merge
done

现在执行删除指令,由于内部资源已碎锁,它们会随街区一并消失:

Terminal window
kubectl delete namespace cert-manager traefik --force --grace-period=0

💀 第四阶段:物理超度 (清理僵尸命名空间)

Section titled “💀 第四阶段:物理超度 (清理僵尸命名空间)”

如果执行完第三步,使用 kubectl get ns 发现它们依然死死卡在 Terminating 状态,说明遭遇了**“僵尸命名空间”**。动用最后的 API 接口直连手段:

Terminal window
kubectl proxy &

2. 发射核弹指令,清空命名空间的 Finalizers

Section titled “2. 发射核弹指令,清空命名空间的 Finalizers”

直接调用 API 接口,强行抹除 Namespace 最后的执念:

Terminal window
# 针对 cert-manager
curl -H "Content-Type: application/json" -X PUT --data-binary "{\"kind\":\"Namespace\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"cert-manager\"},\"spec\":{\"finalizers\":[]}}" http://localhost:8001/api/v1/namespaces/cert-manager/finalize
# 针对 traefik
curl -H "Content-Type: application/json" -X PUT --data-binary "{\"kind\":\"Namespace\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"traefik\"},\"spec\":{\"finalizers\":[]}}" http://localhost:8001/api/v1/namespaces/traefik/finalize
Terminal window
# 确认僵尸已经彻底消失
kubectl get ns
# 杀掉后台开启的 kubectl proxy
kill %1

💡 架构师总结 走完这完整的四阶段,你的集群就真正回到了**“像素级纯净”**的状态。所有的 Role、ServiceAccount 和全局权限都已化为灰烬。现在,你可以放心地去推送你配置了 OpenDNS 5353 端口 的新图纸,享受一次零报错的全新丝滑安装体验!

K8s 证书避坑指南:如何安全地测试并申请 Let's Encrypt 泛域名证书

在 Kubernetes 集群中,使用 cert-manager 配合 Let’s Encrypt 自动签发免费的 HTTPS 泛域名证书,是目前的业界标配。

但是,无数新手在第一次配置时都会踩进一个**“死亡陷阱”**:Let’s Encrypt 对正式接口有极其严格的速率限制。如果你因为 DNS 配置填错、Token 权限不对等原因导致申请失败超过 5 次,你的域名将被拉黑封锁 1 个小时;如果反复重试,甚至会被封锁 7 天!

为了避免“坠机”,最标准、最专业的做法是:先用测试服 (Staging) 跑通流程,再切换到正式服 (Production)。

今天这篇文章,就带你完整走一遍这个“演习到转正”的标准全流程。


🚦 第一阶段:使用 Staging (测试服) 进行实战演习

Section titled “🚦 第一阶段:使用 Staging (测试服) 进行实战演习”

Let’s Encrypt 提供了一个专门的 Staging 测试环境,它的请求限制极其宽松,你可以随便报错、随便重试,绝对不会封锁你的域名。

我们的第一步,就是写一份指向测试服的图纸。

创建一个名为 cluster-issuer-and-cert.yaml 的文件。请注意看代码中的注释,有 3 个地方是测试服独有的

# 1. 声明一个测试服的“发证机关” (ClusterIssuer)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging # 🧪 标志 1:命名为 staging
spec:
acme:
# 🧪 标志 2:必须使用 Staging 的 API 地址!
server: [https://acme-staging-v02.api.letsencrypt.org/directory](https://acme-staging-v02.api.letsencrypt.org/directory)
email: your-email@example.com # ⚠️ 替换为你的真实邮箱,用于接收过期通知
privateKeySecretRef:
# 🧪 标志 3:测试服专用的账号私钥名称,千万别和正式服重名
name: letsencrypt-staging-account-key
solvers:
- dns01:
cloudflare: # 这里以 Cloudflare DNS 验证为例
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
---
# 2. 提交一份“证书申请单” (Certificate)
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: my-website-tls
namespace: default # ⚠️ 替换为你网关 (如 Traefik/Nginx) 所在的命名空间
spec:
secretName: my-website-tls # 最终生成的证书 Secret 名称
issuerRef:
# 🧪 标志 4:指定上面那个测试服发证机关来处理这张单子
name: letsencrypt-staging
kind: ClusterIssuer
commonName: "*.example.com" # ⚠️ 替换为你的域名
dnsNames:
- "example.com"
- "*.example.com"

将上面的配置应用到集群中:

Terminal window
kubectl apply -f cluster-issuer-and-cert.yaml

接下来,我们不需要去浏览器里看,直接在终端里盯着它的状态:

Terminal window
# 监控证书状态 (将 default 换成你自己的 namespace)
kubectl get certificate my-website-tls -n default -w
  • 成功标志:当你看到 READY 这一列从 False 变成了 True,恭喜你,演习大获全胜!这证明你的 DNS Token 和底层网络链路已经 100% 跑通。

  • 终极查验 (彻底放心):如果你想彻底确认这张申请下来的证书到底是谁发的,建议使用以下兼容性最强的“临时文件法”进行查验:

    Terminal window
    # 1. 提取 Secret 中的证书内容,解码后存入临时文件
    kubectl get secret my-website-tls -n default -o jsonpath='{.data.tls\.crt}' | base64 -d > /tmp/cert_verify.crt
    # 2. 调用 openssl 物理查验该文件并过滤颁发者
    openssl x509 -in /tmp/cert_verify.crt -text -noout | grep "Issuer"
    # 3. 查完即焚,删除临时文件
    rm /tmp/cert_verify.crt

    预计输出结果:

    • 演习环境 (Staging):输出包含 Issuer: ..., CN=(STAGING) Artificial Apricot R3

      这证明 Let’s Encrypt 已经认可了你的域名所有权,并把测试用的“假证书”发给你了。

    • 正式环境 (Production):输出包含 Issuer: ..., CN=R3 (没有 STAGING 字样)。

      这说明你的正式绿锁已经下发。

🚀 第二阶段:销毁假证,正式转正 (Production)

Section titled “🚀 第二阶段:销毁假证,正式转正 (Production)”

既然流程已经跑通,我们就可以放心大胆地去正式服领真绿锁了。

回到你刚才的 cluster-issuer-and-cert.yaml 文件,把那 4 个测试服的标志全部改掉:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod # ✅ 1. 改为 prod
spec:
acme:
# ✅ 2. 换成 Let's Encrypt 的正式服 API
server: [https://acme-v02.api.letsencrypt.org/directory](https://acme-v02.api.letsencrypt.org/directory)
email: your-email@example.com
privateKeySecretRef:
name: letsencrypt-prod-account-key # ✅ 3. 换个正式的工作证名称
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: my-website-tls
namespace: default
spec:
secretName: my-website-tls
issuerRef:
name: letsencrypt-prod # ✅ 4. 向正式服机关提交申请
kind: ClusterIssuer
commonName: "*.example.com"
dnsNames:
- "example.com"
- "*.example.com"

再次应用配置:

Terminal window
kubectl apply -f cluster-issuer-and-cert.yaml

2. 【核心必做】物理销毁旧证书!

Section titled “2. 【核心必做】物理销毁旧证书!”

这是无数人翻车的一步! 虽然你修改了申请单,但 Kubernetes 发现你的系统里已经存在一个叫 my-website-tls 的 Secret(刚才测试服发的那个),并且还没过期,它很可能不会去触发重新签发

你必须手动把那个假证书“撕毁”,倒逼 cert-manager 重新去正式服申请:

Terminal window
# 删除测试服生成的假证书 Secret
kubectl delete secret my-website-tls -n default

删掉假证书后,cert-manager 会立刻反应过来:“哎呀,证书没了,而且现在的发证机关变成了 Prod,我得赶紧去申请一张新的!”

再次查看状态:

Terminal window
kubectl get certificate my-website-tls -n default -w

由于之前的网络链路已经验证过完全没问题,这次的申请会极快通过。当状态再次变为 READY: True 时,刷新你的浏览器页面(建议使用无痕模式清除缓存)。

那把坚不可摧、受全球所有设备信任的 HTTPS 绿色小锁,就已经完美挂在你的域名上了!


💡 总结: 无论是个人折腾还是企业级部署,先用 Staging 测试,再切 Prod 签发,最后 delete secret 强制重签,这套“三步走”战略能帮你避开 99% 的证书签发雷区。

ArgoCD 密码重置:如何恢复遗忘的 admin 登录凭证

在日常运维中,如果遗忘了 ArgoCD Web 面板中 admin 账户的自定义登录密码,将导致无法通过图形界面管理应用。

ArgoCD 将 admin 账户的密码哈希值存储在名为 argocd-secret 的 Kubernetes Secret 资源中。当我们在 UI 界面修改密码时,系统实际上是在更新该 Secret 内部的 admin.password 字段。如果该字段被清空或移除,ArgoCD 将自动回退,允许使用系统在初始化时生成的默认密码进行登录。

基于这一底层逻辑,我们可以通过命令行强制重置密码状态。


请确保你的终端已连接到目标 K3s 集群,且具备操作 argocd 命名空间的高级权限。

使用 kubectl patch 命令,强行从存储配置的 Secret 中剔除自定义密码记录。这相当于清除了系统中后续添加的身份验证覆写规则。

Terminal window
kubectl patch secret argocd-secret -n argocd --type=json -p='[{"op": "remove", "path": "/data/admin.password"}]'

执行成功后,终端应返回 secret/argocd-secret patched

密码字段移除后,需要重启 ArgoCD 的前端 Server 组件,使其强制重新读取底层的 Secret 状态。

Terminal window
kubectl rollout restart deploy argocd-server -n argocd

等待几秒钟,确保新的 Pod 成功启动并接管流量。

此时,ArgoCD 已经回退到出厂安全状态。我们需要从专门存储初始密码的 Secret (argocd-initial-admin-secret) 中提取密码的 Base64 原文并进行解码。

Terminal window
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d && echo

请复制终端输出的这串随机字符串,这是你目前的唯一登录凭证。

  1. 浏览器访问你的 ArgoCD 面板。
  2. 账号输入 admin,密码粘贴刚刚提取的初始字符串。
  3. 登录成功后,导航至页面左侧的 User Info 选项卡。
  4. 点击 Update Password,输入当前密码(初始密码)并设定一个新的、已妥善记录的自定义密码。

完全卸载 ArgoCD:级联删除与集群环境重置

核心概念:级联删除 (Cascading Deletion)

Section titled “核心概念:级联删除 (Cascading Deletion)”

在 GitOps 架构中,如果需要彻底重置测试环境或完全弃用当前的部署方案,我们需要将 ArgoCD 及其部署的所有业务应用一并清理。

ArgoCD 在创建 Application 资源时,默认会注入 resources-finalizer.argocd.argoproj.io 终结器 (Finalizer)。当执行删除操作时,ArgoCD 控制器会根据该终结器,主动向集群发送指令,依次删除该应用所关联的全部底层资源(如 Deployment、Service、ConfigMap 等)。这就是级联删除

为了实现彻底卸载,我们必须保留这些终结器,并让 ArgoCD 控制器先完成全部业务资源的回收,然后再卸载 ArgoCD 自身组件。


步骤 1:触发业务资源的级联删除

Section titled “步骤 1:触发业务资源的级联删除”

首先,直接删除 argocd 命名空间下的所有 Application 资源。 警告:此操作不可逆。执行后,由 ArgoCD 管理的所有实际业务(如 Portainer、数据库等集群应用)将立即开始停止并被销毁。

Terminal window
kubectl delete applications --all -n argocd

由于涉及大量底层资源的终止与销毁,该过程可能需要数分钟。通过以下命令实时监控 Application 的状态:

Terminal window
kubectl get applications -n argocd -w

当命令输出为空,或提示 No resources found in argocd namespace. 时,表明所有被托管的业务资源已彻底清理完毕。

业务资源清理完成后,使用当初部署时相同的官方清单反向卸载 ArgoCD 控制器及相关组件:

Terminal window
kubectl delete -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

步骤 4:清理底层全局资源 (CRD 与命名空间)

Section titled “步骤 4:清理底层全局资源 (CRD 与命名空间)”

最后,删除遗留的自定义资源定义 (CRD) 和专属命名空间。

1. 删除专属 CRD: (注意:如果集群内还有其他命名空间在使用 ArgoCD,请勿执行此步骤)

Terminal window
kubectl delete crd applications.argoproj.io applicationsets.argoproj.io appprojects.argoproj.io 2>/dev/null

2. 删除命名空间:

Terminal window
kubectl delete namespace argocd

如果在上述流程中遇到命令卡死或资源无法删除的情况,请参考以下处理方案:

如果 kubectl delete applications 长时间无响应,通常是因为某些底层资源(如持久化存储卷)无法正常释放,导致 ArgoCD 控制器无法完成终结器逻辑。 可以通过强制移除 Finalizer 来跳过等待:

Terminal window
kubectl get apps -n argocd -o name | xargs -I {} kubectl patch {} -n argocd -p '{"metadata": {"finalizers": null}}' --type merge

如果最后一步删除 argocd 命名空间时一直卡在 Terminating 状态,说明 Kubernetes API 垃圾回收遭遇死锁。可使用 API 强制注销指令直接清除(需确保已安装 jq 工具):

Terminal window
kubectl get namespace argocd -o json | \
jq '.spec.finalizers = []' | \
kubectl replace --raw "/api/v1/namespaces/argocd/finalize" -f -

完成以上步骤后,执行 kubectl get all -A,确认集群内已无相关组件残留,集群状态重置完成。

拆除赛博防盗门:如何安全卸载 Sealed Secrets

🏗️ 拆迁前的致命警告 (The OwnerReference Trap)

Section titled “🏗️ 拆迁前的致命警告 (The OwnerReference Trap)”

在动用挖掘机之前,包工头必须先弄清楚 Kubernetes 底层极其冷酷的垃圾回收逻辑:属主引用 (OwnerReferences)

当我们使用 Sealed Secrets 时,Controller 会读取我们提交的乱码 (SealedSecret 资源),并在同命名空间下生成一个真实的密码 (Secret 资源)。 在这个过程中,Controller 会给真实的 Secret 打上一个烙印:“你的主人是那个 SealedSecret”。

致命后果: 如果你直接卸载 Sealed Secrets 并删除了它的 CRD(自定义资源定义),集群里所有的 SealedSecret 会瞬间消失。K8s 垃圾回收器一看“主人死了”,就会把底层真实的 Secret 全部一起拉去陪葬。 你的数据库、网盘、可视化面板会在下一次重启时,因为找不到密码而全部崩溃瘫痪!


🔐 第一阶段:跑路前的保命操作 (备份主密钥)

Section titled “🔐 第一阶段:跑路前的保命操作 (备份主密钥)”

无论你是想重装集群,还是想迁移到其他加密方案(如 SOPS 或 External Secrets),备份主密钥都是重中之重。丢失了这把私钥,你存在 GitHub 上的所有加密乱码将永远变成解不开的死数据。

打开终端,从系统核心区 (kube-system) 提取你的主密钥并保存到本地安全的地方:

Terminal window
# 提取带有特定标签的主密钥 Secret,并导出为 YAML
kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml > sealed-secrets-master-key-backup.yaml

✂️ 第二阶段:无损剥离与拆除 (Safe Teardown)

Section titled “✂️ 第二阶段:无损剥离与拆除 (Safe Teardown)”

如果你希望在卸载 Sealed Secrets 之后,集群里的应用依然能拿着现有的明文密码继续运行(无损拆除),你需要遵循以下步骤。

步骤 1:解除密码的“主仆契约”

Section titled “步骤 1:解除密码的“主仆契约””

我们需要切断真实 SecretSealedSecret 之间的属主绑定。这可以通过批量 Patch 抹除 ownerReferences 来实现(需要宿主机安装 jq 工具)。

执行以下硬核除名脚本(遍历所有命名空间):

Terminal window
# 找出所有由 SealedSecret 生成的真实 Secret,并切断它们的从属关系
kubectl get secrets --all-namespaces -o json | jq '.items[] | select(.metadata.ownerReferences != null) | select(.metadata.ownerReferences[].kind == "SealedSecret") | .metadata.namespace + " " + .metadata.name' | tr -d '"' | while read -r ns name; do
echo "正在解放密码: $ns / $name"
kubectl patch secret $name -n $ns --type=json -p='[{"op": "remove", "path": "/metadata/ownerReferences"}]'
done

执行完毕后,所有的业务密码都变成了自由的 K8s 原生对象,不再受 Sealed Secrets 牵连。

步骤 2:纯手工物理强拆解密守卫

Section titled “步骤 2:纯手工物理强拆解密守卫”

既然密码已经自由,且我们准备对集群进行彻底重置,我们就不再依赖任何自动化工具,直接使用 K8s 原生命令进行“物理强拆”。

我们需要把部署在核心区 (kube-system) 的 Controller、Service 以及配套的 RBAC 权限全部清理干净。为了精准打击且不误伤其他核心组件,我们利用标签 (Label) 进行批量扫荡:

Terminal window
# 1. 拆除运行中的核心建筑(Pod、Service、Deployment 等)
kubectl delete all -l app.kubernetes.io/name=sealed-secrets -n kube-system
# 2. 清扫遗留的系统权限(Role、ClusterRole 等),不留任何幽灵后门
kubectl delete clusterrole,clusterrolebinding -l app.kubernetes.io/name=sealed-secrets 2>/dev/null
kubectl delete role,rolebinding -l app.kubernetes.io/name=sealed-secrets -n kube-system 2>/dev/null

(💡 监理提示:如果你当初是直接使用原生的 helm install 命令安装的,这里也可以更优雅地使用 helm uninstall sealed-secrets -n kube-system 一键卸载。但使用上述的标签扫荡法作为兜底,是最为彻底的。)

为了保证赛博堡垒的绝对干净,我们需要把底层的 CRD 协议和留存的主密钥彻底铲平。

1. 销毁加密协议图纸 (CRD):

Terminal window
# 这会删除集群中所有的 SealedSecret 乱码对象,但因为我们在步骤 1 解除了绑定,真实 Secret 不会受影响
kubectl delete crd sealedsecrets.bitnami.com

2. 销毁集群中的主密钥: 虽然 Controller 死了,但主密钥可能还静静地躺在 kube-system 里。

Terminal window
kubectl delete secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key

巡查集群状态:

Terminal window
kubectl get pods -n kube-system | grep sealed

如果没有任何输出,恭喜你!这扇“赛博防盗门”已经被你完美拆除,而且大楼里的所有业务都在照常营业。

跑路但不拆楼:如何安全卸载 ArgoCD 且保留所有业务应用

🏗️ 核心概念:非级联删除 (Non-Cascading Deletion)

Section titled “🏗️ 核心概念:非级联删除 (Non-Cascading Deletion)”

在赛博堡垒中,有时我们只想重装或卸载 ArgoCD 本身(比如迁移 GitOps 工具,或者清理测试环境),但绝不希望它把正在运行的业务应用(比如我们的存储大坝、可视化面板)一起删掉。

ArgoCD 在部署应用时,默认会给资源打上一个叫 resources-finalizer.argocd.argoproj.io 的终结器(相当于一根引爆线)。如果你直接执行 kubectl delete namespace argocd,它会顺着这根线把底层真实的 Pod 和 Service 全部“爆破”掉,甚至会导致整个命名空间卡死在 Terminating 状态。

要做到**“监工跑路,大楼照常营业”**,我们必须在拆除指挥中心前,偷偷剪断所有的引爆线。


✂️ 无损拆除流程 (Safe Teardown Steps)

Section titled “✂️ 无损拆除流程 (Safe Teardown Steps)”

为了绝对的安全,我们将采用**“暗杀级”**施工法:先切断监工的大脑,再从容拆除。

步骤 1:瘫痪指挥中心大脑 (停止控制器)

Section titled “步骤 1:瘫痪指挥中心大脑 (停止控制器)”

这是应对复杂 GitOps 架构最关键的一步。我们必须先把 ArgoCD 的应用控制器副本数缩容为 0,让其失去对集群状态的监听和自愈能力。

Terminal window
kubectl scale statefulset argocd-application-controller -n argocd --replicas=0

(💡 监理提示:执行后,ArgoCD 变成了“瞎子和聋子”,绝对不会再产生任何反扑动作。)

步骤 2:从容剪断所有应用的“引爆线”

Section titled “步骤 2:从容剪断所有应用的“引爆线””

大脑死机后,我们就可以毫无顾忌地强制抹除所有 Application 对象的 Finalizer。

打开终端,执行这行“批量剪线”脚本:

Terminal window
kubectl get apps -n argocd -o name | xargs -I {} kubectl patch {} -n argocd -p '{"metadata": {"finalizers": null}}' --type merge

(💡 监理提示:执行完后,这些应用就已经彻底脱离 ArgoCD 的“同归于尽”机制了。)

步骤 3:销毁 GitOps 档案 (删除 Application 对象)

Section titled “步骤 3:销毁 GitOps 档案 (删除 Application 对象)”

既然引爆线已经没了,且没有控制器来阻挠,我们现在就可以安全地删掉 ArgoCD 里的那些“项目档案”了。

Terminal window
kubectl delete apps --all -n argocd

这条命令会瞬间执行完毕,而你集群里的真实业务应用完全不会受到任何影响,它们已经变成了 K8s 的原生脱管应用。

步骤 4:拆除主体建筑 (反向执行安装脚本)

Section titled “步骤 4:拆除主体建筑 (反向执行安装脚本)”

清空了档案后,我们就可以按图索骥,用当初浇筑时的官方图纸进行反向拆除,把 ArgoCD 的 Redis 缓存、API 服务等组件清理掉:

Terminal window
kubectl delete -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

步骤 5:深度清理残骸 (删除 CRD 与命名空间)

Section titled “步骤 5:深度清理残骸 (删除 CRD 与命名空间)”

最后一步,把划拨给 ArgoCD 的专属园区,以及它留在 K8s 底层的那些自定义资源定义(CRD)连根拔起。

1. 彻底粉碎专属 CRD: (⚠️ 警告:这会删除全集群所有命名空间下的 ArgoCD 相关资源,确保你没有其他的 ArgoCD 实例在使用它们!)

Terminal window
kubectl delete crd applications.argoproj.io appprojects.argoproj.io applicationsets.argoproj.io

2. 抹平命名空间:

Terminal window
kubectl delete namespace argocd

🧰 进阶排障:应对“强拆”中的钉子户 (Troubleshooting)

Section titled “🧰 进阶排障:应对“强拆”中的钉子户 (Troubleshooting)”

在实际的集群拆迁中,K8s 的垃圾回收机制偶尔会发生死锁。如果你在执行上述步骤时卡住了,请使用以下“重火力”工程手段。

1. 漏网之鱼:AppProject 的终结器卡死

Section titled “1. 漏网之鱼:AppProject 的终结器卡死”

除了 Application,ArgoCD 的“项目对象” (AppProject) 也带有防删机制。如果你发现删除一直卡住,请再补上这一刀:

Terminal window
# 剪断所有项目的引爆线
kubectl get appprojects -n argocd -o name | xargs -I {} kubectl patch {} -n argocd --type json -p='[{"op": "remove", "path": "/metadata/finalizers"}]' 2>/dev/null

2. 清扫游荡的幽灵:全局权限残留

Section titled “2. 清扫游荡的幽灵:全局权限残留”

ArgoCD 拥有控制整个集群的极高权限,这意味着它创建的某些 ClusterRole 是不受命名空间限制的。拆除后,我们可以用标签(Label)进行一次全局扫荡:

Terminal window
kubectl delete clusterrole -l app.kubernetes.io/part-of=argocd 2>/dev/null
kubectl delete clusterrolebinding -l app.kubernetes.io/part-of=argocd 2>/dev/null

3. 终极黑魔法:强杀卡死的 Namespace

Section titled “3. 终极黑魔法:强杀卡死的 Namespace”

如果所有的图纸都已经清理完毕,但最后执行 kubectl delete namespace argocd 时依然无限卡在 Terminating 状态。这意味着 K8s 底层的 API Server 陷入了死循环,通常是因为残留的无法解析的资源阻塞了垃圾回收。

请掏出这段极其危险但也极其有效的 API 强制注销指令(需要宿主机已安装 jq 工具):

Terminal window
kubectl get namespace argocd -o json | \
jq '.spec.finalizers = []' | \
kubectl replace --raw "/api/v1/namespaces/argocd/finalize" -f -

这条指令会直接绕过正常的垃圾回收流程,粗暴地把该园区的终结器全部清空,Namespace 瞬间就会灰飞烟灭!


至此,ArgoCD 已经被你彻底从集群中抹除,你可以使用 kubectl get pods -A 巡查一圈。

你会发现,argocd 命名空间已经彻底消失,且没有任何幽灵权限残留。但你之前通过它部署的真实业务,依然在各自的命名空间里稳健运行。现在,你可以随时重新安装一个崭新的 ArgoCD,再把它们重新“收编”回流水线中!

赛博拆迁办:Headlamp 与网络组件的无痕卸载

在赛博工地里,会建高楼只是基本功,懂得如何干净利落地拆除才是老手的自我修养。Kubernetes 集群就像有洁癖的强迫症患者,如果留下一堆不再使用的孤儿资源(Orphaned Resources),不仅占用节点性能,还可能引发潜在的安全和网络冲突。

今天,“赛博拆迁办”正式进场。我们将按照“逆向工程”的逻辑,把上一篇部署的 Headlamp 仪表盘、权限通行证以及“四大天王” Service 全部连根拔起,做到真正的拔线走人、片甲不留

拆除之前,一定要确认我们即将操作的命名空间(Namespace)和资源名称,避免误伤友军。我们将要清理的内容包含:

  1. Helm 部署的 my-headlamp 实例。
  2. 手搓的管理员权限(ServiceAccount、Secret、ClusterRoleBinding)。
  3. 网络压测时留下的四大 Service。

我们将利用之前保留的“施工图纸(YAML文件)”来进行反向拆除。这也是 GitOps 理念的一大优势:图纸即状态。当你想要销毁什么,只需将 apply 换成 delete

  1. 卸载核心建筑:移除 Headlamp 应用

    既然当初是用对讲机(Helm)呼叫的空投,现在也用它来撤场。执行以下命令,Helm 会自动分析并清理属于 my-headlamp 的所有关联资源(如 Deployment、内置 Service 等)。

    Terminal window
    helm uninstall my-headlamp --namespace kube-system

    预期输出:release "my-headlamp" uninstalled。此时监控室的主体结构已坍塌。

  2. 拔除网络管线:销毁“四大天王” Service

    LoadBalancer 还在占用发牌员的内网 IP,NodePort 还在宿主机上开着洞。果断切断它们,归还给 Cilium:

    Terminal window
    kubectl delete -f all-services.yaml

    (如果你之前没保存文件,也可以像拆除权限那样,直接指定名字进行强制拆除:)

    Terminal window
    # 备用方案(无图纸强制拆除)
    kubectl delete svc headlamp-nodeport headlamp-loadbalancer fake-headlamp-ext -n kube-system

    预期输出会显示这几个 Service 被 deleted。Cilium 会瞬间感知并回收底层的 L2 ARP 广播和跨节点端口映射。

  3. 吊销上帝通行证:清理 RBAC 与 Secret

    虽然应用不在了,但我们手搓的集群最高权限账号还留在 kube-system 里。这在安全规范中是绝对的“高危隐患”。直接拿原图纸反向操作:

    Terminal window
    kubectl delete -f headlamp-admin.yaml

    (如果你之前没保存文件,也可以用命令行逐个击破:)

    Terminal window
    # 备用方案(无图纸强制拆除)
    kubectl delete clusterrolebinding headlamp-admin
    kubectl delete serviceaccount headlamp-admin -n kube-system
    kubectl delete secret headlamp-admin-token -n kube-system
  4. 清理仓库图纸源 (可选)

    如果你是个极致的强迫症,希望本地电脑也不留痕迹,可以把 Headlamp 的 Helm 仓库源从本地一并删掉:

    Terminal window
    helm repo remove headlamp

老规矩,拆完必须验收。执行以下命令,看看现场有没有留下垃圾:

Terminal window
# 检查 Pod 是否已销毁
kubectl get pods -n kube-system | grep headlamp
# 检查网络 Service 是否已清理
kubectl get svc -n kube-system | grep headlamp
# 检查越权角色绑定是否已吊销
kubectl get clusterrolebinding | grep headlamp

如果上面三条命令敲下去,什么也没有输出,恭喜你,这片赛博空地已经彻底清理干净,网络拓扑也恢复到了最纯粹的状态。

包工头语录:能够随时随地一键搭建,也能胸有成竹一键销毁,才是掌控 Kubernetes 的终极自由!准备好这片干净的土地,我们要准备迎接下一个重磅工程了。

赛博拆迁办:安全切断外接大水管 (卸载 NFS 动态供应器)

在赛博工地,拆迁工作也分危险等级。如果说卸载 Longhorn 相当于“定向爆破承重墙”,那么卸载 NFS 动态供应器仅仅相当于**“辞退了一个水管工”**。

因为 NFS 的实际数据都安全地躺在你远端的 TrueNAS 或群晖里,K3s 集群内运行的仅仅是一个负责“自动建文件夹和接管子”的调度程序(Provisioner)。所以,它的卸载过程非常轻松且无痛。

尽管如此,为了保证集群状态的绝对干净,包工头还是建议你按照标准协议进行拆除。

⚠️ 拆除前置:清退依赖水管的租客

Section titled “⚠️ 拆除前置:清退依赖水管的租客”
安全规范

在辞退水管工之前,最好先确认集群里是否还有应用正在使用 nfs-client 这个存储类(StorageClass)。

如果还有应用在用,当你卸载了供应器后,这些应用原本挂载的旧硬盘虽然还能读写,但未来如果它们意外重启或者你需要扩容,由于失去了“水管工”的调度,它们可能会陷入异常状态。

检查指令:

Terminal window
# 查看是否还有绑定到 nfs-client 的 PVC(存储声明)
kubectl get pvc -A | grep nfs-client

(如果输出为空,说明水管已经全部闲置,可以放心开拆。如果还有业务在使用,请先决定是保留业务,还是将其连同 PVC 一并删除。)


  1. 呼叫 Helm 拆迁队 (Uninstall Provisioner)

    因为我们是使用 Helm 规范化部署的,直接用一条命令就可以把水管工(Pod)、它的权限(RBAC)以及图纸代号(StorageClass)全部带走:

    Terminal window
    helm uninstall nfs-provisioner -n kube-system

    预期输出:release "nfs-provisioner" uninstalled。此时,K8s 已经失去了自动在 NAS 上划分目录的能力。

  2. 打扫物理 NAS 的残余数据 (Manual Cleanup)

    还记得我们在上一篇配置 values.yaml 时,特意加了一个防爆机制 archiveOnDelete: true 吗?

    正是因为这个机制,即使你之前在 K8s 里删除了测试用的 PVC,水管工也不会真的去删你 NAS 里的数据,而是会将那个文件夹重命名,加上 archived- 前缀。

    现在,水管工已经被我们辞退了,这部分“赛博垃圾”就需要你手动去清理了:

    • 登录你物理 NAS 的后台(TrueNAS / 群晖)。
    • 打开文件管理器,进入你分配给 K8s 的那个共享根目录(例如 /mnt/pool/k8s_nfs)。
    • 你会看到一些名字带有 archived- 开头的文件夹。如果你确认这些数据都已经没用了,直接在 NAS 后台右键 -> 删除即可,彻底释放物理空间。
  3. 清理本地 Helm 仓库 (可选)

    如果你有极度的强迫症,不想在本地电脑上留下任何痕迹,可以顺手把官方的图纸源也删掉:

    Terminal window
    helm repo remove nfs-subdir-external-provisioner

最后,敲下这行命令,看看 K8s 的“存储物资局”里还有没有这张图纸:

Terminal window
kubectl get storageclass

如果输出的列表中已经找不到 nfs-client,且终端里没有任何关于 nfs-provisioner 的报错,那么恭喜你,这根外接大水管已经被安全、干净地彻底切断!

赛博拆迁办:安全定向爆破 Longhorn 分布式存储大坝

在 Kubernetes 的世界里,无状态应用(如之前部署的 Headlamp)卸载起来就像拔掉 U 盘一样简单。但是,存储组件是集群的“承重墙”

Longhorn 作为底层存储大坝,掌管着所有容器的命脉数据。为了防止新手误操作导致“删库跑路”的悲剧,Longhorn 官方在底层上了一道极其死板的“物理锁”。如果你直接暴力执行卸载,整个 longhorn-system 命名空间会永远卡在 Terminating(挂起)状态,你的 K8s 集群将陷入无尽的死锁僵局。

今天,包工头就结合官方的卸载与排错指南,带你按照正规的“定向爆破协议”,一步步安全、干净地拆除这座大坝,并解决拆除过程中可能遇到的所有疑难杂症。

⚠️ 拆除前置警告:清退所有租客!

Section titled “⚠️ 拆除前置警告:清退所有租客!”
极度危险

在执行任何卸载命令前,为了防止损坏集群,官方强烈建议: 你必须手动删除所有正在使用 Longhorn 卷的 Kubernetes 工作负载(包括 PersistentVolume, PersistentVolumeClaim, StorageClass, Deployment, StatefulSet 等)。 一旦拆除开始,所有基于 Longhorn 创建的虚拟硬盘数据将瞬间灰飞烟灭!请务必提前做好数据备份。


  1. 解除数据自毁保护锁 (The Safety Catch)

    这是卸载 Longhorn 最核心、最不可省略的一步。默认情况下 deleting-confirmation-flag 是关闭的,卸载任务会直接报错拦截。我们需要强制告诉 Longhorn 控制器:“我确定要销毁一切”。

    在终端敲入这行指令,修改核心配置,允许删除操作:

    Terminal window
    kubectl -n longhorn-system patch -p '{"value": "true"}' --type=merge lhs deleting-confirmation-flag

    预期输出:setting.longhorn.io/deleting-confirmation-flag patched。保护锁现已解除。

  2. 呼叫 Helm 拆迁队 (Uninstall Release)

    保护锁解除后,我们就可以正常呼叫 Helm 执行反向拆除了。它会自动释放 Cilium 分配的 LoadBalancer IP,并遣散所有节点的存储引擎容器。

    Terminal window
    helm uninstall longhorn -n longhorn-system

    稍等片刻,直到终端提示 release "longhorn" uninstalled

  3. 清理大坝废墟 (Delete Namespace)

    确认 Helm 卸载完毕后,我们将整个存储专区物理抹除:

    Terminal window
    kubectl delete namespace longhorn-system
  4. 打扫物理宿主机的残渣 (Node Data Cleanup)

    注意:这一步需要到你所有的 3 台 Ubuntu 物理机/虚拟机上分别执行! 虽然 K8s 里的组件删除了,但 Longhorn 之前在你的宿主机磁盘上生成的物理数据块依然保留着。为了彻底归还磁盘空间,直接使用 rm 大法:

    Terminal window
    sudo rm -rf /var/lib/longhorn
  5. 恢复 Linux 内核秩序 (可选)

    如果你确定这几台机器以后再也不碰基于 iSCSI 的存储,可以把之前我们立下的“开机规矩”撤销掉。

    Terminal window
    sudo rm /etc/modules-load.d/longhorn.conf
    sudo systemctl disable --now iscsid

🛠️ 疑难杂症与抢修指南 (Troubleshooting)

Section titled “🛠️ 疑难杂症与抢修指南 (Troubleshooting)”

在赛博工地,意外总是难免的。如果你在卸载过程中遇到了卡死、报错,或者突然“手滑”后悔了,请参考以下官方急救方案:

💊 症状 1:手滑卸载了,但我不想删!(取消卸载)

Section titled “💊 症状 1:手滑卸载了,但我不想删!(取消卸载)”

如果你不小心执行了 helm uninstall(且当时没开保护锁导致它卡在 uninstalling 状态),你可以利用 Helm 的时光机功能紧急回档。

抢修指令:

Terminal window
# 1. 查找 Longhorn 卸载前的最后一个正常版本号 (REVISION)
helm list -n longhorn-system -a
# 2. 假设查到上一个正常版本是 1,执行强制回滚:
helm rollback longhorn 1 -n longhorn-system

提示 Rollback was a success! 代表大坝抢修成功,数据保住了!

💊 症状 2:Namespace 一直卡在 Terminating,CRD 删不掉

Section titled “💊 症状 2:Namespace 一直卡在 Terminating,CRD 删不掉”

这是 Kubernetes 卸载存储组件最常见的恶疾:Finalizer 幽灵锁。因为底层引擎已经被你删了,K8s 还在傻傻等待底层引擎来确认删除这些 CRD(自定义资源),从而形成死锁。

抢修指令(暴力清除所有 Longhorn 状态): 执行以下脚本,它会遍历所有 Longhorn 的 CRD,强行抹除它们的 Finalizer,然后连根拔起。

Terminal window
# 批量强拆:剥夺所有 Longhorn 资源的终结器
for crd in $(kubectl get crd | grep longhorn.io | awk '{print $1}'); do
kubectl get $crd -n longhorn-system -o name 2>/dev/null | xargs -I {} kubectl patch {} -n longhorn-system -p '{"metadata":{"finalizers":null}}' --type merge 2>/dev/null
done

💊 症状 3:执行清理脚本时报错 Webhook 找不到

Section titled “💊 症状 3:执行清理脚本时报错 Webhook 找不到”

如果你在执行上面那个清理脚本时,K8s 抛出了类似这样的错误: Internal error occurred: failed calling webhook "validator.longhorn.io"... service "longhorn-admission-webhook" not found

这是因为残缺的卸载过程把 Webhook 服务删了,但注册表里还留着它的名字,导致 K8s 每次修改资源都想去请求一个不存在的验证服务。

抢修指令: 删除这些拦截请求的幽灵配置,为清理脚本放行:

Terminal window
kubectl delete ValidatingWebhookConfiguration longhorn-webhook-validator
kubectl delete MutatingWebhookConfiguration longhorn-webhook-mutator

删除这两个配置后,重新执行症状 2中的 CRD 清理脚本,即可丝滑通关。


历经波折后,执行最后的扫尾质检:

Terminal window
kubectl get crd | grep longhorn
kubectl get ns | grep longhorn

如果上述命令没有任何输出,恭喜你,这座赛博存储大坝已经被完美定向爆破,所有的顽疾和锁链都已被彻底斩断!

K3s + Cilium 踩坑实录:Longhorn 插件无限重启?揪出 eBPF 路由脑裂的幕后黑手

大家好,我是赛博包工头。欢迎来到赛博工地。

在咱们上一期对 K3s 故障节点进行了“核弹级强拆”并重新加入集群后,本以为可以顺利盖起 Longhorn 存储大坝了。结果,Longhorn 的核心存储特派员 longhorn-csi-plugin 却一直在无限崩溃重启(CrashLoopBackOff)。

表面上看是存储插件坏了,但经过一顿抽丝剥茧,最后发现:这根本不是存储的锅,而是一起典型的网络大动脉断裂案!

今天这篇排错实录,我们就来复盘一下如何解决这种“查号台还活着,但去查号台的路被炸断了”的底层网络脑裂问题。


💥 案发现场:伪装成存储故障的网络崩塌

Section titled “💥 案发现场:伪装成存储故障的网络崩塌”

当时查看 longhorn-csi-plugin 的崩溃遗言(Logs),发现了极其关键的一行报错:

E0419 00:36:26.346354 1 main.go:167] "Error connecting to CSI driver" err="context deadline exceeded"
time="2026-04-19T00:36:30.790662906Z" level=warning msg="Failed to initialize Longhorn API client Get \"http://longhorn-backend:9500/v1\": dial tcp: lookup longhorn-backend on 10.61.0.10:53: read udp 10.60.2.244:53943->10.61.0.10:53: i/o timeout. Retrying"

日志翻译: Longhorn 插件启动时,需要找集群内部的“114查号台”(CoreDNS,IP 为 10.61.0.10:53)去查询总控中心的地址。结果请求发出去后,如同石沉大海,直接报了 i/o timeout(连接超时)。因为拿不到总控地址,插件干脆拒绝启动,最后被 Kubelet 当做死进程给毙了。


🕵️‍♂️ 抽丝剥茧:派出侦察兵探路

Section titled “🕵️‍♂️ 抽丝剥茧:派出侦察兵探路”

为了验证是不是真的连不上 DNS,我跑到后台看了一眼 CoreDNS 的状态,发现它分明是 1/1 Running,活得好好的,安安稳稳地跑在 k3s-master-02 节点上。

接着,我拉起了一个带 nslookup 工具的 busybox 侦察兵进行极限测试:

Terminal window
kubectl run -i --tty --rm debug-net --image=busybox --restart=Never -- nslookup kubernetes.default

返回的结果非常绝望:

;; connection timed out; no servers could be reached

结论:查号台活得好好的,但去找查号台的路,全断了。


🧠 底层逻辑:大动脉为什么会断?

Section titled “🧠 底层逻辑:大动脉为什么会断?”

这要归咎于咱们这套集群的高级架构。

在安装 K3s 时,我们加上了 --disable-kube-proxy 参数。这意味着 Kubernetes 传统的基于 iptables 的内部流量转发机制被废弃了。全集群的 Service IP(包括内部 DNS 地址 10.61.0.10),全靠 Cilium 的 eBPF 机制在 Linux 内核底层进行拦截和高效转发。

但是!咱们刚才对 k3s-master-03 执行了“核弹级强拆”,导致整个集群的 Cilium 路由映射表(BPF Maps)出现了严重的数据断层和脑裂。

Cilium 彻底迷失了方向,不知道该怎么把 UDP 53 端口的请求跨节点发给 k3s-master-02 上的 CoreDNS,于是网络包在内核层直接被无情丢弃。底层网络一断,上层依赖内部域名的 Longhorn 存储插件自然跟着全军覆没。


🛠️ 抢修方案:网络管线“大换血”

Section titled “🛠️ 抢修方案:网络管线“大换血””

既然病根在底层的路由表缓存,咱们不需要重装,只需要强制集群对网络管线进行一次滚动重启(Rollout Restart),让 eBPF 重新绘制整张全网路由表即可。

请依次执行这排雷三把斧

第一斧:强制重建 Cilium 网络映射表

Section titled “第一斧:强制重建 Cilium 网络映射表”

让所有节点的 Cilium 特派员重新扫描集群,重写内核里的路由规则:

Terminal window
kubectl rollout restart ds cilium -n kube-system

(敲完后喝口水等个半分钟,让 3 个节点的 Cilium Pod 挨个重启完成)

第二斧:顺手重启查号台 (CoreDNS)

Section titled “第二斧:顺手重启查号台 (CoreDNS)”

为了防止 CoreDNS 本身残留什么僵尸缓存,给它也来一记还我漂漂拳:

Terminal window
kubectl rollout restart deploy coredns -n kube-system

第三斧:再次派出侦察兵验证验收

Section titled “第三斧:再次派出侦察兵验证验收”

等上面的网络和 DNS Pod 都处于 Running 状态后,重新敲一遍之前的测试命令:

Terminal window
kubectl run -i --tty --rm debug-net --image=busybox --restart=Never -- nslookup kubernetes.default

💡 预期结果: 只要这次没有报 timed out,而是瞬间返回了类似 Server: 10.61.0.10Address: 10.61.0.10:53 的解析结果(即便带了 NXDOMAIN 的小 Bug 也不影响),就说明全集群的大动脉彻底打通了!

大动脉一通,之前卡在 CrashLoopBackOfflonghorn-csi-plugin 就会瞬间找回组织,自动停止报错并挂载成功。

包工头施工笔记: 排查 K8s 故障时,如果上层应用报“连接超时”,不要急着重装应用。拔出网络排错“侦察兵”测一下底层连通性,往往能事半功倍。“遇到 eBPF 脑裂,一键 Rollout 重启网络插件”,这是赛博工地里必须掌握的保命神技!

K3s 高可用集群踩坑实录:节点加入失败死锁?四步“核弹级强拆”彻底清理脏数据

大家好,我是赛博包工头。欢迎回到赛博工地。

最近在给 HomeLab 打 K3s 高可用(HA)集群地基的时候,遇到了一个非常搞心态的坑:新加入的控制节点(比如 Master 03)死活连不上主集群,一直处于 NotReady 或者疯狂重启的状态。

如果你去查日志(journalctl -u k3s),会发现满屏都在刷类似这样的报错:

rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: context deadline exceeded" failed to get etcd status

很多兄弟遇到这种情况,第一反应是:“卸载重装!” 结果跑完官方的卸载脚本,再重新安装,发现问题依旧,还是同样的报错!

今天,我们就来扒一扒这个坑的底层原因,并给出一套“挫骨扬灰”级别的强拆重装方案。


🔍 案情分析:为什么卸载重装没用?

Section titled “🔍 案情分析:为什么卸载重装没用?”

罪魁祸首在于:K3s 官方提供的卸载脚本 /usr/local/bin/k3s-uninstall.sh “太温柔了”

为了防止用户手残误删掉宝贵的生产数据,这个官方脚本在卸载程序时,故意保留了 /var/lib/rancher/k3s 这个核心数据目录(里面存着本地的 ETCD 数据库文件)。

这就导致了一个致命的逻辑死锁:

  1. 你的节点之前因为某种原因加入集群失败,本地生成了一份残缺或错误的 ETCD 数据。
  2. 你执行了官方卸载脚本,卸载了 K3s 程序。
  3. 你重新执行加入命令。
  4. 新安装的 K3s 程序一启动,直接读取到了上一波残留的“脏数据”。它拿着这套旧的、错误的身份凭证去向主集群报到,立刻又被主集群的安保机制拒之门外,当场死锁崩溃。

所以,想要真正重装,我们必须把这台机器上的残骸彻底炸平


请严格按照以下 4 步流程操作,少一步都不行

第一步:执行官方卸载(停机熄火)

Section titled “第一步:执行官方卸载(停机熄火)”

首先,在出故障的节点(例如 Master 03)上,把 K3s 进程停掉并执行常规卸载:

Terminal window
/usr/local/bin/k3s-uninstall.sh

(如果是 Agent 节点,执行 /usr/local/bin/k3s-agent-uninstall.sh)

第二步:手动清理物理残骸(🔥 最关键的一步)

Section titled “第二步:手动清理物理残骸(🔥 最关键的一步)”

这是官方脚本漏掉的,也是破除死锁的核心。我们必须把它的老巢彻底炸平。 依然在 故障节点(Master 03) 上执行:

Terminal window
sudo rm -rf /var/lib/rancher/k3s
sudo rm -rf /etc/rancher/k3s
sudo rm -rf /var/lib/kubelet

⚠️ 警告:执行完这一步,该节点上的所有 K3s 本地数据将彻底灰飞烟灭,不可恢复。

第三步:去主节点注销“僵尸户口”

Section titled “第三步:去主节点注销“僵尸户口””

因为 Master 03 之前卡死过,主集群可能还保留着它的“僵尸档案”。为了防止主集群出于安全防范拒绝它重新加入,我们需要去主节点把它踢掉。

登录到健康的 主节点(例如 Master 01,10.0.10.10),执行:

Terminal window
kubectl delete node k3s-master-03

第四步:重新注入灵魂(满血复活)

Section titled “第四步:重新注入灵魂(满血复活)”

现在,Master 03 已经是一张纯洁的白纸,主集群里也没有了它的黑历史。 回到 故障节点(Master 03),重新粘贴你规划好的加入命令


执行完第四步后,在终端里等个大概 30 秒。然后切回你的本地电脑或主节点,敲一下查岗命令:

Terminal window
kubectl get nodes

彻底清理并重新部署 Portainer:解决存储与权限冲突

在通过 Helm 将 Portainer 的底层存储从 local-path 更改为 longhorn 时,直接执行 helm upgrade 通常会导致升级失败。这主要由以下三个原因造成:

  1. PVC Immutable 报错:Kubernetes 规范限制了已绑定 PVC 的 storageClassName 是不可变的,不能直接修改覆盖。
  2. 全局权限冲突:Helm 尝试创建全局的 ClusterRoleBinding 时,会因为系统中存在历史安装遗留的同名资源而报错。
  3. 手动资源残留:通过 kubectl apply 手动创建的 Service(例如手动配置的 LoadBalancer)不在 Helm 的管理范围内,helm uninstall 不会自动将其回收。

为了保证新配置能够顺利应用,必须先彻底清理命名空间下的所有关联资源,然后执行干净的重装。以下是完整的操作步骤。


注意: 执行以下清理命令将删除该节点上 Portainer 的所有历史数据。请在操作前确认数据已备份或不再需要。

首先,通过 Helm 卸载现有的 Portainer 实例:

Terminal window
helm uninstall portainer -n portainer

2. 删除命名空间下的所有资源及 PVC

Section titled “2. 删除命名空间下的所有资源及 PVC”

由于 Helm 卸载操作会保留 PVC 以及未被其管理的手动资源,需要使用以下命令强制清空 portainer 命名空间:

Terminal window
kubectl delete all --all -n portainer
kubectl delete pvc --all -n portainer

ClusterRoleClusterRoleBinding 是集群级别的资源,不包含在特定命名空间内。需要手动删除它们以解决后续的权限冲突报错:

Terminal window
kubectl delete clusterrolebinding portainer
kubectl delete clusterrole portainer

执行以下命令检查命名空间,确认环境已完全清空:

Terminal window
kubectl get all,pvc -n portainer

正常情况下,终端应返回:No resources found in portainer namespace.


环境清理完毕后,使用配置好 Longhorn 存储的 portainer-values.yaml 文件重新执行 Helm 安装命令:

Terminal window
helm install portainer portainer/portainer \
-n portainer \
--create-namespace \
--set persistence.storageClass=longhorn \
--set nodeSelector=null \
-f portainer-values.yaml

第三阶段:验证部署与恢复服务

Section titled “第三阶段:验证部署与恢复服务”

安装完成后,检查 Pod 和 PVC 的状态。确认 PVC 已经绑定到 longhorn,并且 Pod 处于 Running 状态:

Terminal window
kubectl get pods,pvc,svc -n portainer -o wide

2. 恢复手动创建的 Service (视情况执行)

Section titled “2. 恢复手动创建的 Service (视情况执行)”

如果你之前的外网访问依赖于手动创建的 Service(例如名为 portainer-lb-manual 的资源),并且该配置没有整合进 Helm 的 values.yaml 中,你需要重新应用该配置文件来恢复服务的对外暴露:

Terminal window
# 请将文件名替换为你实际使用的 yaml 文件
kubectl apply -f portainer-lb-manual.yaml

执行完毕后,即可通过分配的 IP 地址访问 Portainer 并完成初始管理员密码的设置。

架构师的外科手术:破解 kube-vip 启动死循环

💡 前置联动说明 本文是对 K3s 高可用集群浇筑 (HA Setup) 章节中“补丁 2”配置的深度原理解析。

  • 🕵️ 发现异常:如果不加该补丁,仅照抄基础部署清单,kube-vip 容器会无限 CrashLoopBackOff,日志疯狂报错 Timeout(连接 API Server 超时)。
  • 🧠 根本原因:底层网络插件(Cilium)未完全就绪,或者 Kube-proxy 被卸载时,组件向集群内部虚拟 IP(10.61.0.1)寻址产生的“先有鸡还是先有蛋”的死循环。
  • 🛠️ 解决方案:通过注入宿主机物理回环地址 (127.0.0.1) 强行搭桥,实现控制面与虚拟网络的彻底解耦。

在打造这套赛博堡垒的高可用地基时,如果直接照搬基础的部署清单,我们大概率会在 kube-vip 的启动阶段摔得头破血流。今天,包工头就带你从底层扒开这个“补丁”的硬核逻辑,复盘我们是如何斩断这个死循环的。


💣 灾难复盘:为什么会疯狂 Timeout?

Section titled “💣 灾难复盘:为什么会疯狂 Timeout?”

在默认情况下,Kubernetes 集群里的任何 Pod(包括 kube-vip)想要呼叫“总部”(API Server),都会去请求一个内部的虚拟 IP(比如我们配置的 10.61.0.1 这个 ClusterIP)。

但这就产生了一个致命的死循环

  1. kube-vip 启动了,它需要和另外两台机器商量谁当“带头大哥”(Leader Election),这必须读写 API Server 的数据。
  2. 于是 kube-vip 向上级汇报,把请求发给了虚拟网关 10.61.0.1
  3. 但是! 此时底层的 Cilium 网络插件可能正在重启,或者还没完全就绪;又或者我们在浇筑阶段强行卸载了 kube-proxy,导致这个虚拟 IP 10.61.0.1 根本没人给它做底层转发。
  4. 结果kube-vip 找不到总部,一直苦苦等待响应,最后只能抛出绝望的 Timeout 并崩溃重启。大门失守,高可用形同虚设。

🗝️ 破局之法:精准搭桥 (外科手术)

Section titled “🗝️ 破局之法:精准搭桥 (外科手术)”

为了打破这个死循环,我们在 kube-vip.yaml 中通过 sed 命令强行注入了这两行环境变量:

- name: KUBERNETES_SERVICE_HOST
value: "127.0.0.1"
- name: KUBERNETES_SERVICE_PORT
value: "6443"

它的作用,就是kube-vip 塞了一张“本地特权 VIP 通行证”!

由于我们在部署时声明了 --inCluster 并开启了 hostNetwork: true(共享宿主机网络),当我们强制注入 127.0.0.16443 后,kube-vip 就不再去那个虚无缥缈的 10.61.0.1 绕弯子了。

它会直接“敲隔壁的门”——通过主机的本地回环网卡(Localhost),呼叫本台物理机上正在运行的 K3s API Server! 管你底层的 Cilium 瘫没瘫痪,管你 K8s 的虚拟网络通没通,我们走的是物理机的地下通道,绝对畅通无阻!这相当于给我们的赛博堡垒建立了一条最高安全级别的消防通道

我们的 KUBERNETES_SERVICE_HOST 注入是在做精密的外科手术。通过注入本地回环地址,实现控制面组件与虚拟网络的硬解耦,彻底斩断网络启动期的死循环锁。

赛博工地的“五分钟定律”:揭秘节点宕机后的 Pod 驱逐真相

在我们的 K3s 高可用堡垒中,我进行了一次“暴力断电”演习:直接关掉了 k3s-master-03 节点。

当时的情况如下:

  • 业务中断:原本运行在 03 节点上的 Headlamp(单副本)瞬间无法访问。
  • 状态迷惑:控制台输入 kubectl get pods -o wide,发现该 Pod 居然还显示为 Running,且依然赖在已经失联的 03 节点上。
  • 没有漂移:想象中的“毫秒级瞬移到其他节点”并没有发生。

这种“僵尸 Pod”现象并非 Bug,而是触发了 Kubernetes 调度架构中极其核心的机制:基于污点的驱逐(Taint-based Eviction)与五分钟定律

📖 官方原典参考:本文底层逻辑均来源于 Kubernetes 官方文档:污点和容忍度


要理解这五分钟,必须先看懂 K8s 官方定义的一对博弈属性,我们直接用命令在案发现场求证:

1. 污点 (Taint) —— 节点的“逐客令”

Section titled “1. 污点 (Taint) —— 节点的“逐客令””

03 节点断电失联时,K8s 控制面会迅速给该节点打上严厉的污点。 🕵️ 求证指令:我们怎么确定节点真的被打上污点了?

Terminal window
# 查看 03 节点的污点状态
kubectl describe node k3s-master-03 | grep Taints

输出结果:你会看到 Taints: node.kubernetes.io/not-ready:NoExecute, node.kubernetes.io/unreachable:NoExecute。 这就是节点的逐客令。NoExecute 效果意味着:不仅新 Pod 不能来,已经在上面的 Pod 必须立刻被驱逐!

2. 容忍度 (Toleration) —— Pod 的“特权通行证”

Section titled “2. 容忍度 (Toleration) —— Pod 的“特权通行证””

面对驱逐令,Pod 能否抗旨?这就看它自身有没有配置对应的容忍度。


🕵️ 破案:这 300 秒是从哪来的?

Section titled “🕵️ 破案:这 300 秒是从哪来的?”

既然污点是 NoExecute (立刻驱逐),为什么 Headlamp 还能在上面赖 5 分钟?这是因为 API Server 在创建普通 Pod 时,默认、悄悄地给它们注入了限时的容忍度。

🕵️ 求证指令:抓出 Pod 身上被暗中注入的“宽限期”

Terminal window
# 查看 Headlamp 这个 Pod 的详细配置
kubectl describe pod <你的-headlamp-pod-名字> -n kube-system | grep -A 5 Tolerations

输出结果:你会赫然发现这样两行配置:

node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s

官方文档解读:“这些自动添加的容忍度意味着 Pod 可以在检测到对应的问题之一时,在 5 分钟内保持绑定在该节点上。” 即:长官让我滚,但我有 300 秒的宽限期。


🏗️ 架构思考:DaemonSet 为什么永远不漂移?

Section titled “🏗️ 架构思考:DaemonSet 为什么永远不漂移?”

Ciliumkube-vip 这类组件,即便过了 5 分钟也不会去别的节点重新拉起。

🕵️ 求证指令:看看 DaemonSet 的容忍度有何不同?

Terminal window
# 使用 -A 10 确保能把 DaemonSet 身上长长的一串特权全打印出来
kubectl describe pod -l app.kubernetes.io/name=kube-vip-ds -n kube-system | grep -A 10 Tolerations

输出结果:你会发现它们也有 not-readyunreachable 的容忍度,但是没有 for 300s 这个时间限制!

官方文档明确指出:“DaemonSet 中的 Pod 被创建时,针对不可达污点添加的 NoExecute 容忍度,将不会指定 tolerationSeconds。这保证了出现上述问题时 DaemonSet 永远不会被驱逐。”


⚡ 极客进阶:掌控驱逐倒计时的“快与慢”

Section titled “⚡ 极客进阶:掌控驱逐倒计时的“快与慢””

作为集群的架构师,你可以通过在 YAML 的 spec.tolerations 中显式声明,来覆盖这默认的 300 秒:

  • 场景 A(无状态前端):想让面板瞬间漂移?配置 tolerationSeconds: 30(警告:容易引发网络抖动时的重建风暴)
  • 场景 B(有状态存储):数据库绑了本地硬盘?必须延长保命!配置 tolerationSeconds: 6000 死等网络恢复。

  1. 架构侧:核心业务增加副本数(Replicas >= 2)配合反亲和性,是应对这 5 分钟断档期的最优雅解法。
  2. 应急侧:演练时不想等待?直接执行强制抹除:kubectl delete pod <POD_NAME> -n <NAMESPACE> --force --grace-period=0
  3. 排查侧:想要系统性掌握这套机制的排查手法?👉 点击查看《赛博工地巡检手册:污点与容忍度排查指令集》

赛博赶羊战术:不改 YAML 强制定向调度 Pod

在赛博堡垒的日常巡检中,我们偶尔会萌生一些“非分之想”:我想把正在运行的 Headlamp 面板,强行从 01 节点挪到 03 节点上去。

按照正规军的做法,我们应该去修改 Deployment 的 YAML 文件,加上 nodeSelector 或节点亲和性。但如果这仅仅是一次临时测试,为了这么点小事去改核心图纸,未免太兴师动众。如果给 0102 节点打 NoSchedule 污点,又会像“核弹打蚊子”一样误伤集群里的其他无辜业务。

今天,包工头教你一招极客圈里经典的“障眼法”——利用节点封锁 (Cordon) 机制的“赛博赶羊战术”


🐑 战术核心:关上多余的门,只留一条路

Section titled “🐑 战术核心:关上多余的门,只留一条路”

Kubernetes 提供了一个专门用于节点临时维护的命令:kubectl cordon(封锁)。 当节点被 Cordon 后,它会被打上 SchedulingDisabled 的标记。这就像给节点挂上了“暂停营业”的牌子:已经在里面吃饭的顾客(运行中的 Pod)不受影响,但绝对不接待新客(新创建的 Pod 无法调度进来)。

我们的战术逻辑极其简单:把不想去的节点全封锁,然后把 Pod 杀掉让它重生。K8s 大脑环顾四周发现只有一台节点“开着门”,就只能乖乖把 Pod 丢进去。


假设我们要把 kube-system 命名空间下的 Headlamp 强行赶到 k3s-master-03 节点。

第一步:封锁非目标节点 (关门)

Section titled “第一步:封锁非目标节点 (关门)”

我们要逼迫目标去 03,所以先把 0102 的大门焊死:

Terminal window
kubectl cordon k3s-master-01 k3s-master-02

验证:此时敲击 kubectl get nodes,你会看到 0102 的 STATUS 变成了 Ready,SchedulingDisabled

第二步:击杀当前 Pod (放狗咬羊)

Section titled “第二步:击杀当前 Pod (放狗咬羊)”

直接删掉当前正在运行的 Headlamp Pod。由于它是由 Deployment 管理的,K8s 会瞬间拉起一个新的替代品。

Terminal window
kubectl delete pod -l app.kubernetes.io/name=headlamp -n kube-system

去看看新重生的 Headlamp 落在哪了:

Terminal window
kubectl get pods -n kube-system -o wide | grep headlamp

因为大脑别无选择,你会发现新的 Pod 已经精准无误地降落在了 k3s-master-03 上!微操成功!

第四步:光速解除封锁 (极其重要!)

Section titled “第四步:光速解除封锁 (极其重要!)”

羊已经进圈了,赶紧把 0102 的大门重新打开,否则你后续部署的任何新业务都会因为找不到节点而卡在 Pending 状态。

Terminal window
kubectl uncordon k3s-master-01 k3s-master-02

验证:再次 kubectl get nodesSchedulingDisabled 标记消失,集群恢复常态。


虽然这套连招玩起来行云流水,极其酷炫,但我必须提醒你:它终究是一张“体验卡”。

Kubernetes 的核心信仰是声明式架构(Declarative)。Cordon 大法只是一次性欺骗了调度器。如果明天 03 节点重启了,或者 Headlamp 意外崩溃了,K8s 大脑在重新调度时,面对三扇全开的大门,依然会随机把 Headlamp 扔回 0102。系统并没有记住你的真实意图。

总结:

  • 临时测试、排查干扰:用 Cordon 大法,干净利落。
  • 永久固化、生产要求:请务必遵守纪律,老老实实去写 nodeSelectornodeAffinity。这才是让 Tech Fortress 坚不可摧的终极规范。