· 阅读约 3 分钟
失败即关闭的数据库迁移
ClickFortify 如何将 Prisma 迁移作为 ArgoCD PreSync 钩子运行,使有问题的迁移在任何新 Pod 启动之前就能阻止发布。
机器翻译。 阅读原文
在大多数部署中,Schema 变更是风险最高的部分。新代码可以在几秒内回滚,而一个卡在两种结构之间的数据库却无法回滚。我是 ClickFortify 的首席架构师,这里的服务运行在 Kubernetes 上,由 ArgoCD 通过 GitOps 部署。我希望迁移步骤具备一个特性:一旦失败,任何新东西都不会运行。
这就是我所说的“失败即关闭”。失败的迁移应当阻止发布,而不是让新 Pod 在一个它们并不预期的 Schema 上启动。
迁移在哪里运行
ClickFortify 的 NestJS 服务通过 Prisma 使用 PostgreSQL。迁移通过 prisma migrate deploy 应用,它会按顺序运行所有待执行的迁移,除此之外不做任何改动。
问题在于在哪里运行它。如果在应用容器启动时运行,每个副本都会争抢执行迁移,而一旦失败,表现出来的就是不断崩溃重启的 Pod。如果在笔记本电脑上手动运行,就要依赖某个人记得去做。两种方式都做不到失败即关闭。
因此,迁移被做成一个 Kubernetes Job,由 ArgoCD 作为 PreSync 钩子运行。ArgoCD 会在应用其余清单之前运行 PreSync 钩子。如果钩子 Job 失败,同步就会中止,新的 Deployment 永远不会被应用。
apiVersion: batch/v1
kind: Job
metadata:
name: migrate
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
backoffLimit: 0
activeDeadlineSeconds: 600
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: registry.example.com/api:sha-abc1234
command: ['npx', 'prisma', 'migrate', 'deploy']这份清单仅作示意,并非从真实清单复制而来。
与发布使用同一镜像
Job 运行的是与它所守护的发布相同的镜像。部署意味着向 ArgoCD 监视的仓库提交一个新的镜像标签,这个标签会同时应用到迁移 Job 和 api Deployment 上。因此,运行的迁移恰好就是随即将上线的代码一同发布的那些迁移。不存在一个可能与代码脱节的独立迁移镜像。
部署状态存放在独立的仓库中,与应用代码分开。这样 ArgoCD 不必在每次刷新时重新克隆庞大的 monorepo,也意味着 ArgoCD 部署的内容是一份小而可审查的标签与清单历史。
不盲目重试
backoffLimit: 0 是有意为之。Kubernetes 很乐意重试失败的 Job,但一个中途失败的迁移可能已经让 Schema 处于半应用状态。不加查看就再次运行可能让情况更糟。只尝试一次,然后停下来,让人去读日志。
activeDeadlineSeconds 为 Job 可以挂起的时间设定了上限,例如卡在某个锁上时,超过这个时间就判定为失败。
保留下来的日志
钩子的删除策略是 BeforeHookCreation。ArgoCD 只有在即将创建下一个 Job 时才会删除旧的 Job。在那之前,失败的 Job 及其 Pod 日志都会保留在集群中,而这恰恰是你最需要它们的时候。如果策略是在失败时删除钩子,就会把证据一并丢掉。
在这些日志中,一次正常的运行很容易辨认:Prisma 会报告它找到的迁移,然后列出已应用的迁移或说明没有待执行的迁移,随后 Job 完成。
顺序可能很重要
在新 Pod 之前运行迁移是正确的默认做法,有时甚至是必须的。设想这样一次发布:新代码不再写入某一列,而这一列的默认值在新的情况下已经不再正确。修复现有行和默认值的迁移必须在新代码启动之前完成,否则新行会拿到过时的默认值,而且没有任何机制去纠正它。
PreSync 钩子天然就提供了这种顺序。
给准备搭建这套机制的人的建议
- 将迁移作为可以中止发布的部署前钩子运行,而不是在应用启动时运行。
- 用与发布相同的镜像标签构建迁移 Job,这样代码和 Schema 就不会脱节。
- 设置
backoffLimit: 0并保留失败 Job 的日志;半应用的 Schema 需要的是人,而不是重试。 - 只通过同步来部署,因为重启会跳过钩子。