昨日消した名前空間だけ戻してほしいという依頼
目標
種類が混在したネームスペース1セットをオブジェクト単位でエクスポートし、そのクラスターでのみ意味を持つフィールドをスクリプトで取り除いたうえで、別のネームスペースへリストアします。古い所有者参照がリストアしたものを静かに削除する様子を実際に確認し、リストアの順序を間違えたときのエラーを確認して、リストアの検証を機械に行わせるようにします。
なぜ重要なのか
etcdスナップショットは、クラスター全体を1つの時点へ戻す道具です。そのため、昨日削除されたネームスペース1つだけを元に戻してほしいという依頼には使えず、別のクラスターへ移すこともできません。こうしたときに必要なのがオブジェクトレベルのバックアップですが、ここにはスナップショットにはない難しさがあります。エクスポートしたオブジェクトを、そのまま戻してはいけません。uid・resourceVersionはそのクラスターでのみ意味を持ち、statusはコントローラーが書き直す値で、ownerReferencesのuidはリストア先のクラスターに存在しません。この最後のものが特に厄介です。ガベージコレクターが所有者のいないオブジェクトと見なして、エラーもイベントもなく削除してしまいます。
ステップ
/root/ops-backup/crd.yamlにCustomResourceDefinitionwidgets.ops.example.comを書いてください。グループはops.example.com、種類はWidget(複数形はwidgets)、ネームスペーススコープ、バージョンはv1(servedとstorageの両方がtrue)で、スキーマに整数spec.sizeがあります。そのあと、/root/ops-backup/scene.yamlに、ネームスペースops-shopのオブジェクト4つを1つのファイルに書いてください。Deploymentcatalog(レプリカ2、ラベルapp: catalog、イメージnginx:1.27.3)、同じ名前のClusterIP Servicecatalog(セレクターapp: catalog、ポート80)、ConfigMapcatalog-config(currency: KRW、page-size: "20")、Widgetw1(spec.sizeは3)です。ネームスペースを作成して、すべて適用してください。- 4つのオブジェクトを、それぞれ
/root/ops-backup/raw/の下に、加工せずそのままエクスポートしてください。deploy.yaml(Deployment catalog)、svc.yaml(Service catalog)、cm.yaml(ConfigMap catalog-config)、widget.yaml(Widget w1)です。kubectl get <종류> <이름> -o yamlの出力に手を加えずに、そのまま保存します(プレースホルダーは種類と名前です)。 /root/ops-backup/clean.shを作成してください。引数で受け取ったYAMLファイル1つを読み取り、クリーンアップした結果を標準出力にだけ出力します。削除するものは、metadataのuid・resourceVersion・creationTimestamp・generation・managedFields・ownerReferences・namespace、最上位のstatus、そしてServiceのspec.clusterIP・spec.clusterIPs・spec.ports[].nodePortです。このスクリプトでraw/の4つのファイルをクリーンアップし、同じ名前で/root/ops-backup/clean/に保存してください。- ネームスペース
ops-shopをまるごと削除し、削除されたことを確認してから、/root/ops-backup/gone.txtに1行で記録してください。ops-shopとNotFoundをタブで区切ります。削除する前に、/root/ops-backup/raw/の4つのファイルがすべてあるかどうかを先に確認してください。 - ネームスペース
ops-shop-restoredを作成し、/root/ops-backup/clean/の4つのファイルを、そのネームスペースへリストアしてください(クリーンアップしたマニフェストにはネームスペースがないので、コマンドで指定します)。Deploymentの2つのPodがRunningになるまで待ってから、/root/ops-backup/restored.tsvに4行で保存してください。Deploymentとcatalog、Serviceとcatalog、ConfigMapとcatalog-config、Widgetとw1を、それぞれタブで区切り、種類名の昇順で並べます。 /root/ops-backup/victim.yamlにConfigMaporphan-victimを書いてください。ネームスペースはops-shop-restored、データはnote: restored-with-stale-ownerで、metadata.ownerReferencesにこのクラスターにない所有者を書きます(apiVersion: v1、kind: ConfigMap、name: catalog-config-old、uidは任意のUUIDです)。適用したあと、そのオブジェクトが消えるまで条件のループで待ってください。そのあと、/root/ops-backup/fixed.yamlに、同じデータを持つConfigMaporphan-fixedを、ownerReferencesなしで書いて適用し、/root/ops-backup/owner.tsvに2行で結果を残してください。orphan-victimとgone、orphan-fixedとaliveを、それぞれタブで区切ります。/root/ops-backup/gadget.yamlに、カスタムリソースGadgetのg1を書いてください。apiVersion: ops.example.com/v1、ネームスペースはops-shop-restored、spec.colorはblueです。まだその種類のCRDがない状態で先に適用してみて、エラーを/root/ops-backup/order-error.txtに保存してください(標準エラー出力を含む)。そのあと、/root/ops-backup/gadget-crd.yamlにCRDgadgets.ops.example.comを書き(グループops.example.com、種類Gadget、複数形gadgets、ネームスペーススコープ、バージョンv1、文字列spec.color)、適用したあと、カスタムリソースをもう一度適用してください。最後に、/root/ops-backup/restore-order.txtに、リストアの順序を4行で書いてください。namespace、crd、custom-resource、workloadを、正しい順序で1行ずつです。/root/ops-backup/inventory.tsvに、リストア一覧を4行で書いてください。<종류>、<이름>、<기대값>をタブ区切りにし、期待値は、Deploymentはspec.replicas、Serviceはspec.ports[0].port、ConfigMapはdataのキーの数、Widgetはspec.sizeです(プレースホルダーは種類、名前、期待値です)。そのあと、/root/ops-backup/verify-restore.shを作成してください。この表を読み取って、ops-shop-restoredの実際のオブジェクトと照合し、一致すればOK <종류>/<이름> <값>、違っていればMISMATCH <종류>/<이름> 기대=<값> 실제=<값>を、標準出力にだけ出力し、1行でも違っていれば、0でないコードで終了する必要があります(プレースホルダーは種類、名前、値です。出力に含まれる韓国語の2つの語は、それぞれ「期待」「実際」を意味します)。その出力を/root/ops-backup/verify.txtに保存してください。
参考
kubectl get <종류> <이름> -o yamlは、サーバーが埋めたフィールドまですべてエクスポートします(プレースホルダーは種類と名前です)。yq eval 'del(...)'は、存在しないパスの削除を指示しても、エラーを出しません。metadata.namespaceを削除しておくと、kubectl apply -n <대상>でリストア先を決められます(プレースホルダーは対象のネームスペースです)。- CRDはクラスタースコープなので、ネームスペースを削除しても残りますが、そのカスタムリソースは一緒に消えます。
- よくある間違い: ownerReferencesをそのまま残してしまい、リストアしたオブジェクトが静かに消えることです。
- よくある間違い: リストアの順序を書き留めておかず、復旧当日に、種類を知らないというエラーに遭遇することです。
- 参考: https://kubernetes.io/docs/concepts/architecture/garbage-collection/
元に戻す1セットを作成する
/root/ops-backup/crd.yamlにCustomResourceDefinitionwidgets.ops.example.comを書いてください。グループはops.example.com、種類はWidget(複数形はwidgets)、ネームスペーススコープ、バージョンはv1(servedとstorageの両方がtrue)で、スキーマに整数spec.sizeがあります。そのあと、/root/ops-backup/scene.yamlに、ネームスペースops-shopのオブジェクト4つを1つのファイルに書いてください。Deploymentcatalog(レプリカ2、ラベルapp: catalog、イメージnginx:1.27.3)、同じ名前のClusterIP Servicecatalog(セレクターapp: catalog、ポート80)、ConfigMapcatalog-config(currency: KRW、page-size: "20")、Widgetw1(spec.sizeは3)です。ネームスペースを作成して、すべて適用してください。
バックアップのラボの半分は、何をバックアップするかを決める作業です。種類が混在した1セットをわざと作成するのには、理由があります。ワークロードと設定とカスタムリソースは、エクスポートするときに削除すべきフィールドも、リストアの順序も、それぞれ異なります。CRDはクラスタースコープなので、ネームスペースを削除しても残ります。
ありのままにエクスポートする
4つのオブジェクトを、それぞれ/root/ops-backup/raw/の下に、加工せずそのままエクスポートしてください。deploy.yaml(Deployment catalog)、svc.yaml(Service catalog)、cm.yaml(ConfigMap catalog-config)、widget.yaml(Widget w1)です。kubectl get <종류> <이름> -o yamlの出力に手を加えずに、そのまま保存します(プレースホルダーは種類と名前です)。
まず加工していない状態を見ることが重要です。ここには、次のステップで削除すべきものがすべて入っています。このクラスターでのみ意味を持つ識別子、コントローラーが埋めた状態、管理フィールドの履歴です。何がなぜ問題なのかは、削除する前に一度見ないとわかりません。
そのクラスターでのみ意味を持つものを削除する
/root/ops-backup/clean.shを作成してください。引数で受け取ったYAMLファイル1つを読み取り、クリーンアップした結果を標準出力にだけ出力します。削除するものは、metadataのuid・resourceVersion・creationTimestamp・generation・managedFields・ownerReferences・namespace、最上位のstatus、そしてServiceのspec.clusterIP・spec.clusterIPs・spec.ports[].nodePortです。このスクリプトでraw/の4つのファイルをクリーンアップし、同じ名前で/root/ops-backup/clean/に保存してください。
metadata.namespaceまで削除する理由は、リストア先をコマンド側で決めるためです。そうすれば、同じバックアップを別のネームスペースや別のクラスターでそのまま使えます。yqのdelは、存在しないパスの削除を指示してもエラーを出さないので、種類ごとにスクリプトを分ける必要はありません。
事故を起こす
ネームスペースops-shopをまるごと削除し、削除されたことを確認してから、/root/ops-backup/gone.txtに1行で記録してください。ops-shopとNotFoundをタブで区切ります。削除する前に、/root/ops-backup/raw/の4つのファイルがすべてあるかどうかを先に確認してください。
ネームスペースの削除は、その中のオブジェクトをすべて持っていきます。CRDはクラスタースコープなので残りますが、そのCRDで作成したカスタムリソースは、ネームスペースと一緒に消えます。削除が終わるまで待ってから記録してください。kubectl delete nsは待ってくれますが、確認は自分で行うほうが安全です。
別のネームスペースへリストアする
ネームスペースops-shop-restoredを作成し、/root/ops-backup/clean/の4つのファイルを、そのネームスペースへリストアしてください(クリーンアップしたマニフェストにはネームスペースがないので、コマンドで指定します)。Deploymentの2つのPodがRunningになるまで待ってから、/root/ops-backup/restored.tsvに4行で保存してください。Deploymentとcatalog、Serviceとcatalog、ConfigMapとcatalog-config、Widgetとw1を、それぞれタブで区切り、種類名の昇順で並べます。
リストア先のネームスペースをコマンドで決められることが、オブジェクトレベルのバックアップの利点です。同じバックアップで検証用のネームスペースに一度立ててみてから、本番のリストアを行う手順を作れます。新しいネームスペースは、defaultサービスアカウントができるまで少し時間がかかります。
古い所有者を残すと、リストアしたものが消える
/root/ops-backup/victim.yamlにConfigMaporphan-victimを書いてください。ネームスペースはops-shop-restored、データはnote: restored-with-stale-ownerで、metadata.ownerReferencesにこのクラスターにない所有者を書きます(apiVersion: v1、kind: ConfigMap、name: catalog-config-old、uidは任意のUUIDです)。適用したあと、そのオブジェクトが消えるまで条件のループで待ってください。そのあと、/root/ops-backup/fixed.yamlに、同じデータを持つConfigMaporphan-fixedを、ownerReferencesなしで書いて適用し、/root/ops-backup/owner.tsvに2行で結果を残してください。orphan-victimとgone、orphan-fixedとaliveを、それぞれタブで区切ります。
バックアップにownerReferencesをそのまま入れておくと、リストアしたオブジェクトが静かに消えます。所有者のuidはクラスターごとに異なり、リストア先のクラスターでそのuidを持つオブジェクトが見つからなければ、ガベージコレクターは所有者がいなくなったものと見なして削除します。エラーもイベントも残らないので、原因を見つけにくい事故です。
順序を間違えると、リストアが失敗する
/root/ops-backup/gadget.yamlに、カスタムリソースGadgetのg1を書いてください。apiVersion: ops.example.com/v1、ネームスペースはops-shop-restored、spec.colorはblueです。まだその種類のCRDがない状態で先に適用してみて、エラーを/root/ops-backup/order-error.txtに保存してください(標準エラー出力を含む)。そのあと、/root/ops-backup/gadget-crd.yamlにCRDgadgets.ops.example.comを書き(グループops.example.com、種類Gadget、複数形gadgets、ネームスペーススコープ、バージョンv1、文字列spec.color)、適用したあと、カスタムリソースをもう一度適用してください。最後に、/root/ops-backup/restore-order.txtに、リストアの順序を4行で書いてください。namespace、crd、custom-resource、workloadを、正しい順序で1行ずつです。
種類を知らないAPIサーバーは、マニフェストをそもそも受け付けません。エラーの文が何を先にインストールするよう言っているのかを、そのまま読んでみてください。リストア手順をドキュメントにするときにこの順序を書き留めておかないと、復旧当日に同じエラーに遭遇し、そのときは今のように余裕がありません。
リストアできたかどうかを、人が目で数えないようにする
/root/ops-backup/inventory.tsvに、リストア一覧を4行で書いてください。<종류>、<이름>、<기대값>をタブ区切りにし、期待値は、Deploymentはspec.replicas、Serviceはspec.ports[0].port、ConfigMapはdataのキーの数、Widgetはspec.sizeです(プレースホルダーは種類、名前、期待値です)。そのあと、/root/ops-backup/verify-restore.shを作成してください。この表を読み取って、ops-shop-restoredの実際のオブジェクトと照合し、一致すればOK <종류>/<이름> <값>、違っていればMISMATCH <종류>/<이름> 기대=<값> 실제=<값>を、標準出力にだけ出力し、1行でも違っていれば、0でないコードで終了する必要があります(プレースホルダーは種類、名前、値です。出力に含まれる韓国語の2つの語は、それぞれ「期待」「実際」を意味します)。その出力を/root/ops-backup/verify.txtに保存してください。
リストアしたあと、人が画面を見てすべてあるかを数えると、必ず1つ見落とします。さらに、オブジェクトがあることと、値が同じであることは別の問題です。名前だけを数えると、レプリカが2から1に減ったリストアも成功に見えます。スクリプトがファイルを直接書き込むと、採点ツールが再実行するときに学習者の成果物を上書きしてしまうので、標準出力にだけ出力してください。