删掉旧版本后对象不见了 - 存储版本迁移的完整步骤
目标
创建一个有两个版本的 CRD,提升存储版本,重新写入所有对象之后,整理 status.storedVersions 并下线旧版本,从头到尾走一遍迁移流程。同时亲眼看看中途 API 服务器会在哪里拦截。
为什么重要
CRD 不是代码,而是 API 契约。要修改一个字段,就要发布新版本,给使用旧版本的人留出迁移的时间之后,才能删除旧版本。这时人们最常漏掉的,是重新写入已经存储的对象。修改存储版本,只是修改“今后要存储的表示形式”,已经在 etcd 中的字节仍然是旧的表示形式。在这种状态下删除旧 schema,就没有办法读取已存储的对象了。status.storedVersions 就是为了防止这种事故而存在的记录,而 API 服务器确实会拒绝把仍留在这个列表中的版本去掉的尝试。本实验先撞上这道安全装置一次,然后按正确的顺序通过。
步骤
- 在
/root/crd-version/tunnel-crd.yaml中编写 CRDtunnels.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-version,并用 v1alpha1 应用/root/crd-version/tunnel-east.yaml(名称t-east,endpoint10.30.0.11,port 4789)和/root/crd-version/tunnel-west.yaml(名称t-west,endpoint10.30.0.12,port 4789)。把应用时返回的警告连同标准错误一起获取,保存到/root/crd-version/deprecation-warning.txt,并把此时的status.storedVersions用一行保存到/root/crd-version/stored-initial.txt。 - 用新版本窗口读取同一个对象——把
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)。 - 把第 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。 - 真正迁移存储版本——把
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。 - 尝试把旧版本从
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=<종료 코드>(占位符为退出码),下面原样贴上服务器给出的语句。 - 现在所有对象都已用 v1beta1 存储,清理记录——用
kubectl patch crd tunnels.net.labhub.io --subresource=status --type=merge把status.storedVersions变成只有["v1beta1"]。把清理之后的值用一行保存到/root/crd-version/stored-pruned.txt。 - 把第 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表示是否接收以该版本发起的请求,storage表示是否以该版本存储。storage 恰好有一个。- 要以特定版本查询,使用
kubectl get <복수형>.<버전>.<그룹> <이름>的形式(占位符依次为复数形式、版本、group、名称)。 status.storedVersions位于 status 窗口中,所以要用kubectl patch … --subresource=status修改。- 这个环境中没有启动 Webhook 服务器的手段,所以
conversion.strategy只涉及到 None。 - 常见错误:只修改存储版本,而不重新写入已有对象。
- 常见错误:先手动删除 storedVersions,使安全装置失效。
- 参考:https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/
- 参考:https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/
让一个类型同时提供两个版本
在 /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 发出的请求会得到找不到资源的回答。存储的对象完好无损,只是从那扇窗口看不到了。检查脚本只向标准输出写入,不要直接改动文件——这样无论运行多少次,都会得到相同的答案。