删掉旧版本后对象不见了 - 存储版本迁移的完整步骤
한국어 원문으로 표시합니다.
목표
두 버전 CRD 를 만들어 저장 버전을 올리고, 모든 오브젝트를 다시 쓴 뒤 status.storedVersions 를 정리하고 옛 버전을 내리는 이전 절차를 처음부터 끝까지 걸어 본다. 중간에 API 서버가 어디서 막아 주는지도 직접 본다.
왜 중요한가
CRD 는 코드가 아니라 API 계약이다. 필드를 하나 바꾸려면 새 버전을 내고, 옛 버전을 쓰던 사람들이 옮겨 갈 시간을 준 뒤에야 지울 수 있다. 이때 사람들이 가장 자주 빠뜨리는 것이 이미 저장된 오브젝트를 다시 쓰는 일이다. 저장 버전을 바꾸는 것은 '앞으로 저장할 표현' 만 바꾸는 일이라, 이미 etcd 에 들어 있는 바이트는 그대로 옛 표현이다. 그 상태에서 옛 스키마를 지우면 저장된 오브젝트를 읽을 방법이 없어진다. status.storedVersions 는 바로 그 사고를 막으라고 있는 기록이고, API 서버는 이 목록에 남아 있는 버전을 빼려는 시도를 실제로 거절한다. 이 실습은 그 안전장치를 한 번 부딪혀 본 다음, 올바른 순서로 통과한다.
단계
/root/crd-version/tunnel-crd.yaml에 CRDtunnels.net.labhub.io를 쓰세요 — 그룹net.labhub.io, 종류Tunnel, 복수형tunnels, 버전 두 개입니다.v1alpha1은served: true·storage: true이고deprecated: true와deprecationWarning을 붙이며 스키마에spec.endpoint(string)·spec.port(integer) 만 둡니다.v1beta1은served: true·storage: false이고 두 필드에 더해spec.mtu(integer,default: 1400)를 둡니다. 적용한 뒤 두 버전의 상태를/root/crd-version/versions-initial.txt에<버전> served=<참거짓> storage=<참거짓>두 줄로 저장하세요.- 네임스페이스
crd-version를 만들고/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)을 v1alpha1 로 적용하세요. 적용할 때 돌아오는 경고를 표준 오류까지 받아/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>과mtu=<spec.mtu 값, 없으면 none>. /root/crd-version/tunnel-crd-beta-storage.yaml에 1단계 CRD 를 복사하되v1alpha1의storage를 거짓으로,v1beta1의storage를 참으로 바꿔 적용하세요. 적용한 뒤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에 한 줄로 저장하세요. /root/crd-version/tunnel-crd-retire.yaml에 4단계 CRD 를 복사하되v1alpha1의served를 거짓으로 바꿔 적용하세요. 그다음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 <복수형>.<버전>.<그룹> <이름>형태를 씁니다. status.storedVersions는 status 창에 있어kubectl patch … --subresource=status로 고칩니다.- 이 환경에는 웹훅 서버를 띄울 수단이 없어
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 를 쓰세요 — 그룹 net.labhub.io, 종류 Tunnel, 복수형 tunnels, 버전 두 개입니다. v1alpha1 은 served: true·storage: true 이고 deprecated: true 와 deprecationWarning 을 붙이며 스키마에 spec.endpoint(string)·spec.port(integer) 만 둡니다. v1beta1 은 served: true·storage: false 이고 두 필드에 더해 spec.mtu(integer, default: 1400)를 둡니다. 적용한 뒤 두 버전의 상태를 /root/crd-version/versions-initial.txt 에 <버전> served=<참거짓> storage=<참거짓> 두 줄로 저장하세요.
한 CRD 안의 버전들은 같은 오브젝트를 다른 창으로 보는 것이라 저장은 딱 하나의 표현으로만 이루어집니다. 그래서 storage 가 참인 버전은 정확히 하나여야 하고, 나머지는 served 만 켜서 읽고 쓰는 창으로 남깁니다. deprecationWarning 은 그 버전으로 요청할 때 클라이언트에게 돌아가는 문장입니다.
폐기된 버전으로 만들면 경고가 돌아온다
네임스페이스 crd-version 를 만들고 /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)을 v1alpha1 로 적용하세요. 적용할 때 돌아오는 경고를 표준 오류까지 받아 /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> 과 mtu=<spec.mtu 값, 없으면 none>.
conversion.strategy 를 따로 적지 않으면 None 이고, None 은 저장된 바이트를 그대로 두고 apiVersion 딱지만 바꿔 보여 줍니다. v1beta1 스키마에는 기본값이 있는 필드가 있는데, 기본값은 '저장할 때' 채워집니다. 이 오브젝트는 아직 어느 버전으로 저장돼 있는지 생각해 보세요.
저장 버전을 올린다
/root/crd-version/tunnel-crd-beta-storage.yaml 에 1단계 CRD 를 복사하되 v1alpha1 의 storage 를 거짓으로, v1beta1 의 storage 를 참으로 바꿔 적용하세요. 적용한 뒤 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=<종료 코드> 이고 그 아래에 서버가 낸 문장을 그대로 붙입니다.
이 거절은 잔소리가 아니라 안전장치입니다. 목록에 남아 있는 버전의 스키마를 지우면 그 표현으로 저장된 오브젝트를 읽을 방법이 없어집니다. 오류 문장이 어느 필드를 지목하는지 정확히 읽어 보세요 — 고쳐야 할 곳이 거기에 적혀 있습니다.
기록을 정리해야 문이 열린다
이제 모든 오브젝트가 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 로는 닿지 않습니다.
옛 창을 닫고 이전 완료를 증명한다
/root/crd-version/tunnel-crd-retire.yaml 에 4단계 CRD 를 복사하되 v1alpha1 의 served 를 거짓으로 바꿔 적용하세요. 그다음 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 으로 보내는 요청은 리소스를 찾을 수 없다는 답을 받습니다. 저장된 오브젝트는 멀쩡히 있는데 그 창으로만 안 보이는 것입니다. 점검 스크립트는 표준출력에만 쓰고 파일을 직접 건드리지 않게 하세요 — 그래야 몇 번을 돌려도 같은 답이 나옵니다.