一次例行的反向代理升级,我服务器上所有站点断了九十秒。配置没动过,升级本身也很顺利。最后的解决办法是给一个文件改个名,至于为什么改名就好了,那是另一篇文章的事。让我记住的是之后的事:原因写进了仓库的 README,一个专门查这种故障的检查从此每天跑两次,而这两样东西都是以 commit 的形式进去的,跟这台机器上的其他东西一样。
这就是 docker-gitops 的出发点:把真实的服务器当成一个程序来运行。上面跑什么,以 Git 为准;改动走 commit 和 push,而不是 SSH 上去东改一下西改一下。最开始只有一台机器,现在有三台了。这篇是这个仓库的一张地图,挑了四个这套做法真正帮上忙的地方来讲。
仓库目录就是运行目录
每台主机一个目录,每个 compose stack 在所属主机下面再占一个目录。stack 就在这个目录里跑:cd 进去,docker compose up -d。仓库和服务器之间没有另一条部署路径,两边也就没机会各走各的。你改的文件,就是容器启动时读的那个文件。
所以手动做过的事藏不住。哪天凌晨两点我上机器临时改了点东西,第二天 git status 里就能看到,而且会一直挂在那里,直到我提交或者丢掉。不存在第三种情况,让服务器默默记着一件仓库不知道的事。
另外两台主机上根本没有仓库的副本,全靠第一台通过 SSH 来驱动。这个单独写了一篇。
四个地方
防火墙是一个有 git 历史的 shell 脚本。 一开始不是这样的。以前我按常规做法持久化规则:iptables-save 存成文件,开机时重放。问题是快照是在 Docker 运行时拍的,Docker 自己的那些按 bridge 生成的规则也一起被冻进去了。有一次重启之后,新建的网络复用了一个旧子网,那些早已不存在的 bridge 的规则又活了过来,这个网络上的容器彼此就连不通了。现在只持久化我手写的规则,放在一个脚本里,开机由一个 oneshot unit 执行。Docker、k3s 和 Cilium 的链由它们自己重建。改一条规则,就是一次 commit。
加一个服务靠的是 skill,不是 wiki 页面。 仓库里有一个 agent skill,也就是一份我的编程 agent 会照着执行的分步说明,能把一个新服务从头走到尾:写 compose stack、建反向代理条目、加仪表盘卡片、提交。例行的步骤交出去,需要判断的部分留给我自己,比如它接入哪个网络、用哪个访问列表保护。以前我也有一份"怎么加服务"的文档,一直想更新一直没更新。skill 不会这样过时,因为每次都会真的跑一遍。
小问题主机自己修,大问题告诉我。 有一个巡检程序每天 09:00 和 21:00 各跑一次,覆盖三台主机加上集群,一共三十来项检查。有些会自己清理:停了一周的容器、悬空镜像、过期的构建缓存。其余的只报警,因为怎么处理得由人来定。我最喜欢的一项来自一次事故:两个容器被 OOM kill 后悄悄重启了,pod 一直是 Running,所以盯着 pod 状态的东西全都没发现。现在专门有一项检查查这个。
README 里有一张地雷图。 根目录的 README 用粗体重复了一条警告:每个 Custom Location 都会把主机名写死在 proxy_pass 里,而字面量 upstream 必须在加载配置时解析成功,否则 nginx 拒绝启动,所有反向代理的站点一起挂掉。开头那九十秒就是这么来的。写下来,是为了让下一个人先撞上警告,而不是先撞上 bug。可警告只对读它的人有用,所以巡检里有一项会对线上配置跑 nginx -t。地雷会先出现在报告里,而不是等到下一次重启才被踩响。
底下那条线
这些东西跑下来,我得出一条规矩:只是拿来跑的软件放 Compose,自己写的软件放 Kubernetes。 Compose 是用来跑别人的东西的,跑起来就不用管。Kubernetes 适合你会不断去改的东西。这个仓库大部分是 Compose,因为我跑的东西大多不是我写的;另外有一个小小的 k3s 集群,因为我确实 在写的那些东西需要一个练手的地方。这一点我是花了代价才学到的:把六个服务搬上 Kubernetes,又全部搬了回来。
从哪里看起
仓库是公开的。先看 README 里那条 Custom Location 的警告,再看 docs/incidents/,大部分规矩都是从这两个地方来的。然后挑一台主机看下去。如果你只有一台机器,又受够了它变成一堆"我以前手动做过的事",这是一条出路。