TT Lab
开始
学习 学习路径 课程

CRD 与 Operator

删掉旧版本后对象不见了 - 存储版本迁移的完整步骤

在 TT Lab 中继续学习

目标

创建一个有两个版本的 CRD,提升存储版本,重新写入所有对象之后,整理 status.storedVersions 并下线旧版本,从头到尾走一遍迁移流程。同时亲眼看看中途 API 服务器会在哪里拦截。

为什么重要

CRD 不是代码,而是 API 契约。要修改一个字段,就要发布新版本,给使用旧版本的人留出迁移的时间之后,才能删除旧版本。这时人们最常漏掉的,是重新写入已经存储的对象。修改存储版本,只是修改“今后要存储的表示形式”,已经在 etcd 中的字节仍然是旧的表示形式。在这种状态下删除旧 schema,就没有办法读取已存储的对象了。status.storedVersions 就是为了防止这种事故而存在的记录,而 API 服务器确实会拒绝把仍留在这个列表中的版本去掉的尝试。本实验先撞上这道安全装置一次,然后按正确的顺序通过。

步骤

  1. 在 /root/crd-version/tunnel-crd.yaml 中编写 CRD tunnels.net.labhub.io——group 为 net.labhub.io,kind 为 Tunnel,复数形式为 tunnels,有两个版本。v1alpha1 为 served: true、storage: true,加上 deprecated: true 和 deprecationWarning,schema 中只有 spec.endpoint(string)和 spec.port(integer)。v1beta1 为 served: true、storage: false,在这两个字段之外再加上 spec.mtu(integer,default: 1400)。应用之后,把两个版本的状态以 <버전> served=<참거짓> storage=<참거짓> 两行的形式保存到 /root/crd-version/versions-initial.txt(占位符依次为版本、true 或 false、true 或 false)。
  2. 创建命名空间 crd-version,并用 v1alpha1 应用 /root/crd-version/tunnel-east.yaml(名称 t-east,endpoint 10.30.0.11,port 4789)和 /root/crd-version/tunnel-west.yaml(名称 t-west,endpoint 10.30.0.12,port 4789)。把应用时返回的警告连同标准错误一起获取,保存到 /root/crd-version/deprecation-warning.txt,并把此时的 status.storedVersions 用一行保存到 /root/crd-version/stored-initial.txt。
  3. 用新版本窗口读取同一个对象——把 kubectl -n crd-version get tunnels.v1beta1.net.labhub.io t-east -o yaml 的输出保存到 /root/crd-version/as-v1beta1.yaml。并在 /root/crd-version/conversion-note.txt 中写两行——apiVersion=<읽은 apiVersion>(占位符为读取到的 apiVersion)和 mtu=<spec.mtu 값, 없으면 none>(占位符为 spec.mtu 的值,没有则为 none)。
  4. 把第 1 步的 CRD 复制到 /root/crd-version/tunnel-crd-beta-storage.yaml,把 v1alpha1 的 storage 改为 false,把 v1beta1 的 storage 改为 true,然后应用。应用之后,把 status.storedVersions 用一行保存到 /root/crd-version/stored-after-flip.txt。
  5. 真正迁移存储版本——把 crd-version 中的所有 Tunnel 用 v1beta1 读出来,原样重新写入即可(kubectl get … -o json | kubectl replace -f -)。两个对象都重新写入之后,把 kubectl -n crd-version get tunnels.v1beta1.net.labhub.io -o yaml 的输出保存到 /root/crd-version/after-rewrite.yaml。
  6. 尝试把旧版本从 spec.versions 中去掉——kubectl patch crd tunnels.net.labhub.io --type=json -p '[{"op":"remove","path":"/spec/versions/0"}]'。把命令的输出和退出码汇总到 /root/crd-version/remove-blocked.txt——第一行是 remove-rc=<종료 코드>(占位符为退出码),下面原样贴上服务器给出的语句。
  7. 现在所有对象都已用 v1beta1 存储,清理记录——用 kubectl patch crd tunnels.net.labhub.io --subresource=status --type=merge 把 status.storedVersions 变成只有 ["v1beta1"]。把清理之后的值用一行保存到 /root/crd-version/stored-pruned.txt。
  8. 把第 4 步的 CRD 复制到 /root/crd-version/tunnel-crd-retire.yaml,把 v1alpha1 的 served 改为 false,然后应用。接着执行 kubectl -n crd-version get tunnels.v1alpha1.net.labhub.io t-east,把结果汇总到 /root/crd-version/retired.txt——第一行是 get-rc=<종료 코드>(占位符为退出码),下面贴上服务器的语句。最后创建 /root/crd-version/version-check.sh——它要把这个 CRD 的 served、storage、stored 各用一行以 <이름>=<값> 的形式打印出来(占位符依次为名称与值);如果 storedVersions 中有不是存储版本的值,就打印 PENDING <버전>(占位符为版本)并以非 0 的退出码结束;如果没有,就打印 MIGRATED 并以 0 结束。把它的输出保存到 /root/crd-version/version-check.txt。

参考

让一个类型同时提供两个版本

在 /root/crd-version/tunnel-crd.yaml 中编写 CRD tunnels.net.labhub.io——group 为 net.labhub.io,kind 为 Tunnel,复数形式为 tunnels,有两个版本。v1alpha1 为 served: true、storage: true,加上 deprecated: true 和 deprecationWarning,schema 中只有 spec.endpoint(string)和 spec.port(integer)。v1beta1 为 served: true、storage: false,在这两个字段之外再加上 spec.mtu(integer,default: 1400)。应用之后,把两个版本的状态以 <버전> served=<참거짓> storage=<참거짓> 两行的形式保存到 /root/crd-version/versions-initial.txt(占位符依次为版本、true 或 false、true 或 false)。

同一个 CRD 中的各个版本,是从不同窗口看同一个对象,所以存储只会以恰好一种表示形式进行。因此 storage 为 true 的版本必须恰好有一个,其余的只打开 served,保留为读写窗口。deprecationWarning 是用该版本发起请求时返回给客户端的文字。

用已弃用的版本创建,会收到警告

创建命名空间 crd-version,并用 v1alpha1 应用 /root/crd-version/tunnel-east.yaml(名称 t-east,endpoint 10.30.0.11,port 4789)和 /root/crd-version/tunnel-west.yaml(名称 t-west,endpoint 10.30.0.12,port 4789)。把应用时返回的警告连同标准错误一起获取,保存到 /root/crd-version/deprecation-warning.txt,并把此时的 status.storedVersions 用一行保存到 /root/crd-version/stored-initial.txt。

弃用警告输出到的是标准错误,而不是标准输出。storedVersions 不在 spec 中,而在 status 中,表示“用这个 CRD 到目前为止实际存储过的版本”。如果到目前只用一个版本存储过,请预想一下里面会是什么。

用新版本读取,会有什么不同

用新版本窗口读取同一个对象——把 kubectl -n crd-version get tunnels.v1beta1.net.labhub.io t-east -o yaml 的输出保存到 /root/crd-version/as-v1beta1.yaml。并在 /root/crd-version/conversion-note.txt 中写两行——apiVersion=<읽은 apiVersion>(占位符为读取到的 apiVersion)和 mtu=<spec.mtu 값, 없으면 none>(占位符为 spec.mtu 的值,没有则为 none)。

不单独写 conversion.strategy 时就是 None,None 会保持存储的字节不变,只改变 apiVersion 标签来展示。v1beta1 的 schema 中有带默认值的字段,而默认值是“存储时”填充的。请想一想这个对象目前是以哪个版本存储的。

提升存储版本

把第 1 步的 CRD 复制到 /root/crd-version/tunnel-crd-beta-storage.yaml,把 v1alpha1 的 storage 改为 false,把 v1beta1 的 storage 改为 true,然后应用。应用之后,把 status.storedVersions 用一行保存到 /root/crd-version/stored-after-flip.txt。

修改这一行,只会改变“今后存储时使用的表示形式”。已经在 etcd 中的对象的字节不变。所以 storedVersions 不会减少,反而会增加——请再读一遍这个列表的含义。

把所有对象各重新写入一遍

真正迁移存储版本——把 crd-version 中的所有 Tunnel 用 v1beta1 读出来,原样重新写入即可(kubectl get … -o json | kubectl replace -f -)。两个对象都重新写入之后,把 kubectl -n crd-version get tunnels.v1beta1.net.labhub.io -o yaml 的输出保存到 /root/crd-version/after-rewrite.yaml。

重新写入是否真的发生了,新版本独有的默认值就是证据。因为以存储版本写入的那一刻,API 服务器会填充这个字段。在对象有几千个的真实生产环境中,不会一次性运行这项工作,而是分批运行——因为每次都会发生 etcd 写入。

API 服务器拦截,说还不能删除

尝试把旧版本从 spec.versions 中去掉——kubectl patch crd tunnels.net.labhub.io --type=json -p '[{"op":"remove","path":"/spec/versions/0"}]'。把命令的输出和退出码汇总到 /root/crd-version/remove-blocked.txt——第一行是 remove-rc=<종료 코드>(占位符为退出码),下面原样贴上服务器给出的语句。

这个拒绝不是唠叨,而是安全装置。如果删除仍留在列表中的版本的 schema,就没有办法读取以那种表示形式存储的对象了。请准确阅读错误语句指出的是哪个字段——需要修复的地方就写在那里。

记录清理之后,门才会打开

现在所有对象都已用 v1beta1 存储,清理记录——用 kubectl patch crd tunnels.net.labhub.io --subresource=status --type=merge 把 status.storedVersions 变成只有 ["v1beta1"]。把清理之后的值用一行保存到 /root/crd-version/stored-pruned.txt。

这次清理是由人来声明“重新写入已经完成”的行为。API 服务器不会替你统计重新写入——所以如果跳过第 5 步直接从这里开始,就会直接变成事故。status 是单独的窗口,用普通的 patch 够不到。

关闭旧窗口,证明迁移已经完成

把第 4 步的 CRD 复制到 /root/crd-version/tunnel-crd-retire.yaml,把 v1alpha1 的 served 改为 false,然后应用。接着执行 kubectl -n crd-version get tunnels.v1alpha1.net.labhub.io t-east,把结果汇总到 /root/crd-version/retired.txt——第一行是 get-rc=<종료 코드>(占位符为退出码),下面贴上服务器的语句。最后创建 /root/crd-version/version-check.sh——它要把这个 CRD 的 served、storage、stored 各用一行以 <이름>=<값> 的形式打印出来(占位符依次为名称与值);如果 storedVersions 中有不是存储版本的值,就打印 PENDING <버전>(占位符为版本)并以非 0 的退出码结束;如果没有,就打印 MIGRATED 并以 0 结束。把它的输出保存到 /root/crd-version/version-check.txt。

关闭 served 之后,该版本的端点本身就消失了,所以向旧 apiVersion 发出的请求会得到找不到资源的回答。存储的对象完好无损,只是从那扇窗口看不到了。检查脚本只向标准输出写入,不要直接改动文件——这样无论运行多少次,都会得到相同的答案。