两个星期里,我把六个自托管服务从 Docker Compose 搬上了 Kubernetes------ArgoCD、供应链安全策略、完整的 GitOps 闭环。然后,在同一天之内,我把六个全部搬了回来,还顺带搬走了第七个:一个原本就生在集群上的站点。这一段几乎没人写。大家写的是风光的迁移,很少有人写撤回,以及让前后两半都说得通的理由。技术上没有任何东西失败。坏掉的是我一直抱着的一个假设:Kubernetes 到底是拿来干什么的。
大家都读过的那一半
往上搬的那一半,网上到处都是,我就压缩着讲。一台 4 核 24 GB 的机器,在一个反向代理后面跑着一堆 compose stack。我在同一台机器上装了 K3s------没有加新硬件,而是跟 compose 服务并排跑在同一个内核上------然后分阶段把服务搬过去。ArgoCD 用 app-of-apps 做根。每个服务一份迁移模板,保证走同一条路。Kyverno 准入策略、Trivy 镜像扫描、SealedSecrets 管凭据。就是让一个集群能靠近生产环境的那一整套机制。
有一条规矩让这次迁移保持诚实:对外的域名和端口永远不变,反向代理始终是锚点。把服务迁进去,在 k3s 上跑通,再把代理规则指过去。后端在底下替换,对外那一面始终没动,每个服务都是一个可以单独回退的步骤。
迁移成功了。工具各司其职,ArgoCD 对每个 Application 都报告 Synced 和 Healthy。然后我把它撤了。
是撤回,不是回滚
所有服务在一天之内回到了 compose:homepage、trilium、dify、vikunja、apprise,还有 LLM 那一套。那个没有 compose 前身的 k8s 原生站点,也一起进了 compose。留在 k3s 上的清单很短:lab-environment、headlamp,以及一个叫 pr-lanes 的 namespace,里面跑着一个名叫 hello 的双层练习应用。
我管这叫撤回,而不是回滚。回滚意味着一次失败的变更之后往回退,而这里什么都没失败。项目建起来的其他东西都保留了------集群底座、GitOps 闭环、供应链策略、service mesh------大部分到现在还在跑。被撤回的只有计划里的一行:把现有的 compose 服务搬进 k8s。
这个决定归结到一个观察。一旦看清了它,其他所有事情都解释得通了。
我迁过去的每一样东西,都不是我自己写的软件。
Kubernetes 只在你自己的软件上回本
这句话是字面意思:从上游镜像仓库拉下来的开源镜像。dify、vikunja、trilium、homepage、apprise、那几个模型服务------没有一行是我写的、迭代过的或者发布过的。就这一个事实,把它们从"为什么要跑 Kubernetes"的每一条理由里都排除了出去。理由有三条,一条比一条糟。
第一条是集成成本。第三方镜像对你的网络策略、CI 流水线、内部 DNS、准入规则毫无兴趣,打包它的人从来没考虑过你的管控层。所以你要么让它半接入地挂着,要么去做集成,而这份工作的全部产出只是让别人封好的盒子能被你的平台看懂。两条路都是实打实的活,产出的东西却没有人会用。一个现成的开源镜像,根本没必要成为你集群的一部分。
第二条更糟,因为整个价值主张在这里蒸发了。金丝雀发布、service mesh、服务之间的 mTLS、渐进式发布------这些东西存在的意义,都是让软件的主人对自己的交付进行精细控制。它们是给生产者用的工具。第三方镜像不是我发布的东西,而是我消费的东西。版本 tag 是别人打的,我唯一能做的决定是要不要跟。我没有自己的发布节奏要调,没有自己的金丝雀要拦,也没有自己的服务间流量要保护------应用内部怎么组织,上游早就定死了。mesh 和金丝雀是为生产者造的,而我是以消费者的身份来的。
第三条让它不再只是理论。供应链策略是整个项目里我最得意的部分:Trivy 扫描每个镜像,准入策略拒绝任何带有"已有修复版本"的严重 CVE 的 pod。对我自己构建的软件,这个闭环会自己合上------升级基础镜像、重新构建、重新部署。对别人构建的软件,"修复 CVE"意味着要维护一份别人镜像的补丁版。
这件事我做过一次。npm 自带的 tar 库里有一个 DoS 漏洞,出现在 homepage 和 trilium 里,而这两个应用的上游都没有发布修复版本。于是我做了包装镜像:拿上游镜像,替换掉自带的 npm,通过带签名的流水线重新构建,扫描干净。成功了。代价是一条新的 CI 流水线、一套签名配置,外加 homepage 的一次大版本升级------因为漏洞库还有一份是应用自身的依赖,包装镜像碰不到。这只是一个 CVE,两个镜像。
接着我准备把扫描策略从审计模式切到强制模式,结果发现 ArgoCD、dify 和实验环境自己的组件都带着没人注意到的、已有修复版本的严重漏洞------只是因为这些 pod 在扫描报告出来之后一直没重启过。如果在全集群强制执行,ArgoCD 下次重建 pod 时就可能被自己的集群拒之门外。唯一安全的做法,是把这道关卡只限定在我自己构建的镜像上。策略引擎替我先得出了那个结论:这道关卡只对我自己写的软件有意义。
服务搬回 compose 的第二天,我删掉了那些包装镜像。那两个容器上已经没有任何东西要求扫描干净了,而它们修补的那个 CVE------只能造成 DoS,没有已知的在野利用------被接受为低风险。安全策略并没有失败。它是为生产者设计的,而我一直把它对准我消费的东西。
那笔账我早就付过了
这些代价都不是假设。它们就躺在我那几周的事故记录里,放在一起读,规律才浮现出来。每一条单看都像互不相干的偶发事件:内存超配,内核把容器 OOM kill 了,pod 却一直显示 Running;Trivy operator 的扫描并发配置写错了 Helm values 的层级,被直接忽略,把磁盘打满;为了让 service mesh 正常工作而收窄的一个 Cilium 参数,悄悄切断了反向代理到所有 k3s 服务的路径;还有一个从 pod 发往 compose 容器的包消失在策略路由黑洞里,最后我选择绕开设计,而不是去修。
四次事故,四个根因,底下是同一个规律。在一台已经跑着 Docker 的单节点上,Kubernetes 不是免费的。它加进来第二个调度器、第二套网络、第二层安全、第二个事实来源,而这些层还得跟原本就在的那些层达成一致------在同一个内核上、同一台机器上。Compose 当然也会出事,我最严重的一次故障就是 compose 侧的反向代理起不来。但它的故障局限在一层之内,你能从头读到尾。在共享的机器上,Kubernetes 的故障是跨层的,表现出来就是"全是绿灯,但结果不对"。
对于你正在积极修改代码的服务,这笔税能换来真东西。对于跑在一台 4 核家用机器上的第三方镜像,它什么也换不来,而我一直在交。
留下来的,就是证据
留在 k3s 上的清单替我把道理讲完了,而且不是我刻意安排的。Headlamp 是别人的软件,但它是 Kubernetes 的管理面板:它存在的意义就是看集群,离开集群无处可去。lab-environment 从第一天起就是为集群构建的,从来没有 compose 版本。剩下的是 pr-lanes 里那个双层的 hello 应用,而在这里,"自己写的"和"消费的"这条线划得更细了,因为这一对里只有一半真正是我的。hello-backend 是我通过自己的 CI 发布的软件。hello-frontend 复用的是从一个已退役的练习应用继承来的签名镜像------它的构建上下文已经没了,今天在跑的那个 digest 根本无法从仓库重新构建出来。
这不是反例,而是撤回决定的缩小版。我自己写的那一半留在 k3s 上,真正用上了隔离的发布通道、mesh、金丝雀和扫描关卡;冻结的、继承来的那一半,恰恰是不属于那里的东西,迟早要换成能构建的版本。哪怕缩小到只剩一个练习应用,实验环境划出的还是迁移时那条线。
没有一个消费型的生产服务留下来。在那台机器上,k3s 从来不是 compose 的继任者。它是实验室:在唯一能让这个平台发挥作用的那类软件上,练习这个平台。凡是我负责运维、别人负责编写的服务,都属于 compose------在那里,"跑起来就别管"就是全部功能,而这恰好是这些服务需要的。
获得一种能力的三种方式
把推理再往前推一层,就跟 Kubernetes 或 Compose 都没关系了。当我需要某种能力时,有三种获得方式,各自对应一类软件。
数据不必归我、也不想承担运维时,买 SaaS。数据必须归我、但软件不必是我写的时,用 compose 自托管。软件本身是我的、而且我会一直改它时,自己写,跑在 Kubernetes 上。
我不得不撤回的错误,是比这三种都差的第四种:用为生产 软件而建的平台,去跑我消费的软件。你为平台付了全价,而跑在上面的软件永远用不上这个平台存在的理由。Kubernetes 提供的一切,都是塑造你自己软件的手段。如果你已经决定不去塑造它------"原样运行别人的镜像"正是这个决定------那么这些手段在你开始之前就已经全部放弃了,剩下的只有运维成本。
分界线不是重要的软件和不重要的软件,甚至也不是软件改得有多频繁:dify 一直在发新版本。分界线是谁掌握变更 。我写的软件按我的节奏、以我塑造的方式变化,所以它配得上为塑造变化而建的基础设施:金丝雀、发布通道、关卡。我消费的软件按上游的节奏变化,以一个新 tag 的形式到来,我要么跟、要么不跟,而最适合它的是那个无聊的地方------跟进新版本就是 docker compose pull && docker compose up -d。dify 对我很重要,但它的变更没有一个是我的。所以它跑在 compose 上。
这套框架才是我真正带走的东西。搬上去,让我知道这个平台能做什么;搬回来,让我知道它是为谁做的。前一半网上到处都是。这篇写的是后一半。