本記事には広告(アフィリエイトリンク)が含まれます。

etcd snapshot+drain【CKA第5回】

広告

新卒インフラエンジニア向け Kubernetes 実践教科書(第2巻 CKA 編)の第5回です。動作確認バージョン: AlmaLinux 10.1 / K8s v1.35.5 / etcd v3.6.6 / etcdutl v3.6.6 / kubeadm v1.35.5(2026-05-23 時点・k8s-cp-01〜03 / k8s-wl-01〜02 / k8s-ops 実機検証済)。第4回までで 5 ノード HA クラスタ(CP×3 + WL×2)を完成させ、fanclub-api を kubeadm HA クラスタへ Helm で移行しました。第5回は「クラスタを守る・直す」スキルを習得する回です。etcd はクラスタの全状態(全リソースの定義・Namespace・Secret・ConfigMap・RBAC など)を保持するキーバリューストアであり、etcd が壊れたらクラスタは機能しません。だから定期的なバックアップと、いざというときの restore 手順の習得が欠かせません。CKA 試験の D1(Cluster Architecture, Installation and Configuration)でも、etcd backup/restore は毎回必ず出題される頻出テーマです。本回で「壊して直す」体験を一度積んでおくと、試験本番で落ち着いて操作できるようになります。第5回終了時点で第1部(クラスタ構築・全5回)が完走します。「構築から運用へ」という大きなフェーズ転換の節目です。

広告

今ここマップ(第5回 / 全16回 / 第1部)

今ここマップ(第2巻 16 回中の現在位置):

今ここ: 第5回 / 全16回(第1部:クラスタ構築)
▓▓▓▓▓░░░░░░░░░░░  31%

第1部(クラスタ構築):       ■■■■■ 5/5 回(完了)← 今ここ
第2部(ワークロード管理):   □□□   0/3 回
第3部(ネットワーク):       □□□   0/3 回
第4部(ストレージ):         □     0/1 回
第5部(監視・運用):         □□    0/2 回
第6部(トラブルシュート):   □□    0/2 回

第5回で学ぶことは次の 6 点です。これがそのまま今回の学習目標になります。

  • etcd の役割(クラスタの全状態を保持するキーバリューストア)と Raft consensus の概念を理解する
  • GitHub Release tarball から etcdctl / etcdutl をインストールし、ETCDCTL_API=3 環境変数を設定できる
  • ETCDCTL_API=3 etcdctl snapshot save で etcd スナップショットを保存し、etcdutl snapshot status で健全性を確認できる
  • etcdutl snapshot restore でスナップショットから復元し、/etc/kubernetes/manifests/etcd.yaml--data-dirvolumes.hostPath.path を更新して etcd を再起動できる
  • kubectl drain --ignore-daemonsets --delete-emptydir-data でノードのワークロードを安全に退避し、kubectl uncordon で復帰させられる
  • HA クラスタでの etcd restore が「全メンバー同時 restore」を要する理由を説明でき、シングル CP シナリオとの違いを理解している

第5回終了時の達成状態を整理します。これが第1部完走の証であり、第2部(ワークロード管理)の前提状態になります。

  • etcdctl / etcdutl が k8s-cp-01 にインストールされ、etcdctl endpoint health で etcd クラスタの健全性を確認できる
  • /backup/etcd-snapshot-20260523.db が k8s-cp-01 に保存されている
  • snapshot save → テスト Namespace 作成 → etcd restore → Namespace 復元の一連を自力で実施できた
  • k8s-wl-02 の drain + uncordon が成功し、ワークロードが k8s-wl-01 に移動 → 復帰までを確認できた
  • 第1部(クラスタ構築・全5回)が完走し、第2部(ワークロード管理)への準備が整っている

第5回のスコープと設計判断 — 「クラスタを壊して直す」体験

実機作業に入る前に、第5回で扱う範囲と扱わない範囲を整理し、本回の 3 つの設計判断を共有します。etcd restore は破壊的操作です。手順を誤るとクラスタ全損になりかねません。だからこそ「何をやるか」「なぜその方法を選ぶか」を先に把握しておくと、各ステップの目的が明確になり、作業中に迷いません。

第5回で「やること」と「やらないこと」

第5回は破壊的操作を含む実機演習が中心です。スコープを明示し、後回しにする範囲は明確なロードマップを示します。

やることやらないこと
etcdctl / etcdutl のインストール(k8s-cp-01)HA 全 3 メンバー同時 restore(→ 第15回)
ETCDCTL_API=3 etcdctl snapshot save によるスナップショット取得kubectl delete node による完全削除(→ 別途解説のみ)
etcdutl snapshot restore + Static Pod manifest 更新etcd の保存時暗号化設定(EncryptionConfiguration)(→ 第3巻 CKS 編)
etcdutl snapshot status による健全性確認
kubectl drain / kubectl uncordon によるノードメンテナンス

本回には 3 つの設計判断が含まれます。いずれも「教材としての最適解」と「CKA 試験本番との整合」を両立する選択です。順に説明します。

設計判断① restore コマンドは etcdutl snapshot restore を主軸とする

本回の restore 演習は etcdutl snapshot restore を正式コマンドとして使用します。理由は 3 つあります。1 つ目は etcd v3.5.0 から etcdutl が正式ツールとして導入され、snapshot restore / snapshot status / defrag の機能が etcdctl から移行したことです。本回でインストールする etcd v3.6.6 の tarball にも etcd / etcdctl / etcdutl の 3 バイナリが同梱されています。2 つ目は kubernetes.io 公式ドキュメント(CKA 試験で参照可能な URL)も etcdutl snapshot restore で記載されていることです。3 つ目は CKA 試験で最新の公式コマンドを覚えることが合格への近道であることです。なお etcdctl snapshot restore は etcd v3.5 で deprecated となり、本シリーズが採用する etcd v3.6 では削除されたため、restore は etcdutl snapshot restore を使用します(第15回の本番警告⑤でも同じ点を扱います)。本教材ではコマンドブロックを etcdutl で統一します。

一方で snapshot save は引き続き etcdctl の役割です。「save は etcdctl、restore と status は etcdutl」と覚えておくと整理しやすくなります。

設計判断② HA restore 演習はシングル CP シナリオで実施する

第5回演習では「k8s-cp-01 上のシングル etcd 体験シナリオ」で restore を実施します。3 メンバー全停止 + 全 restore + --initial-cluster 再設定は概念解説のみとし、演習対象外とします。理由は 3 つあります。1 つ目は CKA 試験本番では「単一 etcd の snapshot restore」シナリオが出題されることです(3 メンバー全停止シナリオは出題されません)。2 つ目は全 CP 停止中は API Server も落ちるため、読者環境でのリスクが高いことです。3 つ目は教育的な複雑度(--initial-cluster / --initial-advertise-peer-urls の引数指定)が第5回の学習目標を超えることです。

HA 全 restore 演習は第15回(Control Plane + etcd トラブルシュート演習)で「意図的な etcd 障害 + restore 手順」のシナリオとして再登場します。本回は概念のみ理解しておけば十分です。

設計判断③ etcdutl のインストール場所は k8s-cp-01 を主軸とする

environment.md の定義では「k8s-ops に etcdctl をインストール」となっていますが、本回の演習は k8s-cp-01 上に etcdutl + etcdctl をインストールして実行することを主軸とします。理由は証明書がローカルにあって最も簡素なこと、そして CKA 試験本番では問題ノード上で etcdctl を直接打つのが通例であり、本番と同一の状況を再現できるためです。

k8s-ops からの利用方法も補足で言及します。k8s-ops にも同じバイナリをインストールし、CP ノードから証明書を scp で取得して使う構成です。ただし環境変数指定が煩雑になるため、本回演習の主軸からは外します。

もう一点、本回では restore 手順のうち「manifest 編集」を sed による一括置換で行います。vi で開いて 3 箇所を手作業で修正する方法も成立しますが、「3 箇所のうち 1 つでも忘れたら restore は失敗する」という落とし穴の重さを考えると、機械的に書き換える運用のほうが安全です。CKA 試験本番でも vi が苦手な受験者には sed の利用を推奨します。試験環境では sed が標準で利用可能であり、置換結果の確認は grep で済みます。

第5回終了時点の各 VM 状態

VM第5回終了時の状態
k8s-cp-01(192.168.1.125)etcdutl + etcdctl インストール済み・snapshot 保存済み・restore 体験済み
k8s-cp-02 / k8s-cp-03変更なし(etcd メンバーとして稼働継続)
k8s-wl-01 / k8s-wl-02drain + uncordon 体験済み(fanclub-api ワークロード正常稼働)
k8s-ops(192.168.1.122)kubectl drain / uncordon の操作元として使用

etcd の役割と HA クラスタでの restore の考え方

実機作業に入る前に、etcd の役割と HA クラスタでの restore が複雑になる理由を概念として理解します。「なぜ本回はシングル CP シナリオなのか」「なぜ HA 全 restore は第15回送りなのか」の背景がここで明確になります。

etcd とは何か — クラスタの全状態を保持するキーバリューストア

etcd は Kubernetes クラスタの全状態を保持するキーバリューストアです。kubectl apply -f deployment.yaml を実行すると、kube-apiserver がリソース定義を etcd に書き込みます。逆に kubectl get pods は、kube-apiserver が etcd から読み出してきた情報を返しているのです。つまり etcd が壊れたら、クラスタは「自分が何者か」を完全に忘れます。Pod も Service も Namespace も、すべて etcd の中にしか存在しません。

kubectl apply のデータフロー — apiserver 経由で etcd に永続化
kubectl apply のデータフロー — apiserver 経由で etcd に永続化

etcd が「何を覚えているか」を整理すると、バックアップの重要性が直感的に理解できます。代表的な保存対象を次の表にまとめます。

データ種別説明
Namespaceクラスタ上の全 Namespace 定義
Deployment / Pod / Service全 K8s リソースの定義と現在のステータス
ConfigMap / Secretアプリ設定・TLS 証明書・API キー
RBAC 設定Role / RoleBinding / ServiceAccount
Node 情報ノード登録情報・Taint / Label / Annotation

これらが etcd から失われると、たとえ Workload Node が無事でも、kube-apiserver は「どの Pod を起動すべきか」を判断できません。Pod のマニフェストも消えているからです。etcd のバックアップは「保険」ではなく「必須運用タスク」だと位置づけてください。

逆に Workload Node 上の Pod プロセスは、etcd のバックアップ対象には含まれません。これは混乱しやすいポイントです。コンテナのファイルシステム、メモリ上の状態、PostgreSQL StatefulSet の DB データ、Pod が書き込んだログファイル — これらは etcd の外にあるため、snapshot save / restore では復元されません。データ層(PV / PVC)のバックアップは別途 Velero(第2巻第14回)や Longhorn のスナップショット(第12回)で行います。「etcd は『定義』をバックアップする・データそのものは別経路」の役割分担を、最初に頭に入れておくと運用設計が描きやすくなります。

Raft consensus と quorum — 3 メンバー HA の合意形成

第4回で構築した HA クラスタの etcd は、3 メンバー stacked etcd 構成(k8s-cp-01/02/03 上に同居)です。3 メンバー構成では Raft consensus アルゴリズムでデータの一貫性を保ちます。書き込みは「過半数(2/3)が合意した場合にのみ確定」というルールで進行します。これを quorum(クォーラム・定足数)と呼びます。

第3回でも触れた etcd メンバー数と障害耐性の関係を、ここで再確認します。

構成障害耐性quorum 数
1 メンバー(dev / test)0 ノード1
3 メンバー(本番最小)1 ノード2
5 メンバー(本番推奨)2 ノード3

本シリーズの 3 メンバー構成では、1 ノード障害までは quorum を維持できます。CP-02 がダウンしても CP-01 と CP-03 で過半数(2/3)を構成できるため、クラスタは動作を続けます。逆に 2 ノードが同時にダウンすると quorum を失い、書き込み不能(読み込みは継続)の状態になります。

HA クラスタで restore が複雑になる理由

quorum の仕組みを理解すると、HA クラスタでの restore が複雑になる理由が見えてきます。3 メンバー HA では「多数決」で整合性を確保するため、1 ノードだけ restore して起動すると、他の 2 メンバーとの間で quorum 選択が行われ、restore 内容が上書きされる可能性があります。これは厳重に注意すべき本番運用上のリスクです。

本番運用警告①: HA クラスタで 1 ノードのみ restore すると quorum で上書きされる

3 メンバー HA 環境で k8s-cp-01 だけ restore して起動すると、CP-02・CP-03 との間で quorum 選択が行われ、restore した内容が新しい多数派データで上書きされる可能性があります。本番で HA restore を行うときは「全メンバーで etcd を停止 → 全メンバーで同じスナップショットから restore → --initial-cluster / --initial-cluster-token を指定して再起動」が必須です。本回は概念のみ理解しておけば十分で、実演習は第15回に委ねます。

正しい HA restore 手順の全体像だけ参考にお伝えします。全 3 メンバーで etcd を停止し、全メンバーで同じスナップショットから restore したうえで、各メンバーで以下のように起動引数を指定します。

etcdutl snapshot restore snapshot.db \
  --name m1 \
  --data-dir /var/lib/etcd-restored \
  --initial-cluster m1=https://192.168.1.125:2380,m2=https://192.168.1.126:2380,m3=https://192.168.1.127:2380 \
  --initial-cluster-token etcd-cluster-1 \
  --initial-advertise-peer-urls https://192.168.1.125:2380

引数の数からも察せられるとおり、HA restore は「3 メンバー分の引数を 1 箇所でも間違えると失敗する」高難度シナリオです。本回(第5回)は「CKA 試験で出題される典型シナリオ:シングル etcd の restore」を確実に習得することに集中します。

本回で学ぶ手順は、HA restore の「下位互換」ではなく「同じ思考プロセスのコア部分」です。シングルでも HA でも「① etcd を停止 → ② 旧データを退避 → ③ etcdutl で復元 → ④ manifest を整える → ⑤ 起動して確認」という 5 ステップは変わりません。違いは「3 メンバー分を同時に行うか・--initial-cluster 系の引数が増えるか」という点に集約されます。土台が固まれば、第15回で扱う HA 全 restore も「シングルを 3 倍にして引数を 3 箇所追加するだけ」と整理できるようになります。

etcdutl / etcdctl のインストールと動作確認

ここから実機作業に入ります。本セクションは GitHub Release tarball から etcdctl + etcdutl をインストールし、etcd クラスタへの疎通を確認するまでが目的です。作業場所は k8s-cp-01(root 権限)です。

Step 1: etcd サーババージョンの確認

クライアントツールはサーバ側 etcd と同一バージョンを使うのが原則です。インストール前に kubeadm が起動している etcd サーバのバージョンを確認します。

実行コマンド(k8s-cp-01 上・root):

# kubectl --kubeconfig=/etc/kubernetes/admin.conf get pods -n kube-system etcd-k8s-cp-01 -o jsonpath='{.spec.containers[0].image}'

実行結果:

registry.k8s.io/etcd:3.6.6-0

etcd v3.6.6 系であることが確認できました。これに合わせて etcdctl / etcdutl v3.6.6 をインストールします。

Step 2: 前提パッケージ(tar)の確認・導入

AlmaLinux 10.1 Minimal Install には tar が含まれていません。tarball を展開する前に dnf で導入します(既に入っている場合は何もしません)。

実行コマンド:

# dnf install -y tar

Step 3: GitHub Release tarball のダウンロードと展開

etcd v3.6.6 の Linux amd64 用 tarball を GitHub Release から取得します。第2回で github.comobjects.githubusercontent.com は alma-proxy の whitelist に登録済みなので、Squid 経由でそのままダウンロードできます。

実行コマンド(k8s-cp-01 上・root):

# mkdir -p /tmp/etcd-install
# cd /tmp/etcd-install
# curl -LO https://github.com/etcd-io/etcd/releases/download/v3.6.6/etcd-v3.6.6-linux-amd64.tar.gz
# tar -xzf etcd-v3.6.6-linux-amd64.tar.gz
# ls etcd-v3.6.6-linux-amd64/

実行結果:

Documentation  README-etcdctl.md  README-etcdutl.md  README.md  etcd  etcdctl  etcdutl

tarball には etcd(サーバ本体)・etcdctl(クライアント・save 担当)・etcdutl(オフラインユーティリティ・restore/status 担当)の 3 バイナリが同梱されています。本回で使うのは後の 2 つです。

3 バイナリの役割分担を整理しておきます。etcd はサーバ本体ですが、kubeadm クラスタでは kubelet が Static Pod として起動する etcd-k8s-cp-01 がこれに相当するため、ここでインストールしたバイナリは使いません(あくまで tarball に同梱されているという認識で十分)。etcdctl はサーバへの問い合わせを通じて操作する「オンラインクライアント」です。snapshot saveendpoint healthmember list など、動作中の etcd と通信するコマンドはこちらを使います。etcdutl は etcd 本体や etcdctl とは独立して動く「オフラインユーティリティ」です。snapshot restoresnapshot statusdefrag など、サーバとの通信を伴わない(または推奨されない)操作はこちらに集約されています。

Step 4: バイナリの配置とバージョン確認

取り出した 2 バイナリを /usr/local/bin/ に配置します。install コマンドはパーミッション(755)の指定とコピーを一度に行えます。

実行コマンド(k8s-cp-01 上・root):

# install -m 755 etcd-v3.6.6-linux-amd64/etcdctl /usr/local/bin/etcdctl
# install -m 755 etcd-v3.6.6-linux-amd64/etcdutl /usr/local/bin/etcdutl
# etcdctl version
# etcdutl version

実行結果(バージョン確認):

etcdctl version: 3.6.6
API version: 3.6
etcdutl version: 3.6.6
API version: 3.6

サーバと同一の v3.6.6 で揃いました。API version が 3.6 になっていることも重要なポイントです。なお AlmaLinux 10.x の sudoers では secure_path/usr/local/bin が含まれない設定の場合があり、sudo etcdctl ... で「command not found」になることがあります。その際は sudo /usr/local/bin/etcdctl ... のようにフルパスで実行してください。

Step 5: ETCDCTL_API=3 の永続設定

etcdctl はデフォルトで v2 API を使う場合があります。明示的に v3 API を有効化するため、環境変数 ETCDCTL_API=3 をシステム全体で永続化します。/etc/profile.d/ 配下にスクリプトを置くと、全ユーザーのログイン時に自動読み込みされます。

実行コマンド(k8s-cp-01 上・root):

# echo 'export ETCDCTL_API=3' > /etc/profile.d/etcdctl.sh
# chmod 644 /etc/profile.d/etcdctl.sh
# source /etc/profile.d/etcdctl.sh
# echo $ETCDCTL_API

実行結果:

3

Step 6: etcd クラスタへの疎通確認

etcdctl で etcd クラスタに接続するには TLS 証明書が必要です。kubeadm が配置した証明書は /etc/kubernetes/pki/etcd/ 配下にあります。--cacert--cert--key の 3 つを指定して endpoint health を確認します。

実行コマンド(k8s-cp-01 上・root・etcd クラスタの疎通確認):

# etcdctl --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  endpoint health

実行結果(自ノード etcd が healthy):

https://127.0.0.1:2379 is healthy: successfully committed proposal: took = 2.345ms

続いて 3 メンバー全員の状態を一覧します。member list サブコマンドを使います。

実行コマンド(k8s-cp-01 上・root・メンバー一覧):

# etcdctl --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  member list

実行結果(3 メンバーが started 状態でリストされる):

5a4e3b2c1d0f9e8a, started, k8s-cp-01, https://192.168.1.125:2380, https://192.168.1.125:2379, false
7b3c4d5e6f1a2b3c, started, k8s-cp-02, https://192.168.1.126:2380, https://192.168.1.126:2379, false
9c5d6e7f8a2b3c4d, started, k8s-cp-03, https://192.168.1.127:2380, https://192.168.1.127:2379, false

3 メンバーすべてが started 状態であり、Peer URL(2380 ポート)と Client URL(2379 ポート)が想定通り公開されていることが確認できます。第4回までに構築した HA etcd クラスタが正常稼働している証拠です。

k8s-ops へのインストール(補足)

同じ手順で k8s-ops にも etcdctl + etcdutl をインストールできます。ただし k8s-ops から etcd に接続するには、k8s-cp-01 から証明書を scp で取得する必要があります。例えば次のように証明書一式をローカルへコピーします。

実行コマンド(k8s-ops 上・developer・補足手順):

$ mkdir -p ~/etcd-certs
$ scp developer@192.168.1.125:/etc/kubernetes/pki/etcd/ca.crt ~/etcd-certs/
$ scp developer@192.168.1.125:/etc/kubernetes/pki/etcd/server.crt ~/etcd-certs/
$ scp developer@192.168.1.125:/etc/kubernetes/pki/etcd/server.key ~/etcd-certs/

セキュリティ上、本番では証明書の取り扱いに細心の注意を払います。秘密鍵(server.key)は読み取り権限を 600 に絞ること、作業後は速やかに削除することが基本です。本回演習は k8s-cp-01 上で完結させ、k8s-ops 経由は補足扱いとします。

CKA 試験 Tips: 証明書オプションの環境変数化

etcdctl の証明書オプション(--cacert / --cert / --key)は毎回指定するため、CKA 試験中は環境変数に事前セットしておくと打鍵数を減らせます。本文中のコマンドは明示のため毎回書きますが、試験では ETCDCTL_CACERT / ETCDCTL_CERT / ETCDCTL_KEY の 3 変数を .bashrc に追記しておくテクニックも有効です。ETCDCTL_ENDPOINTS も同様にセットできます。

snapshot save と健全性確認(etcdutl snapshot status)

etcdctl / etcdutl のインストールが完了したので、いよいよ etcd スナップショットを取得します。本セクションは「① バックアップディレクトリ準備 → ② snapshot save → ③ etcdutl snapshot status による健全性確認 → ④ ファイル属性確認」の 4 ステップで進めます。

Step 1: バックアップディレクトリの準備

スナップショットの保存先として /backup ディレクトリを作成します。本番では別ボリュームや外部ストレージにマウントする運用が望ましいですが、本回は学習用に同一ファイルシステム上に作成します。

実行コマンド(k8s-cp-01 上・root):

# mkdir -p /backup
# chmod 700 /backup
# ls -ld /backup

実行結果:

drwx------ 2 root root 4096 May 23 09:55 /backup

パーミッションは 700(root のみアクセス可)にします。snapshot ファイルには Secret や TLS 証明書のキーも含まれるため、第三者が読めない権限設定が必須です。

Step 2: snapshot save の実行

etcdctl snapshot save でスナップショットを取得します。ファイル名には日付を含めて etcd-snapshot-20260523.db のように管理しやすくします。etcdctl はサーバへの問い合わせを通じてスナップショットを取得する仕組みのため、3 メンバー HA でも片方の endpoint(ここでは 127.0.0.1:2379)に問い合わせれば取得できます。

実行コマンド(k8s-cp-01 上・root):

# etcdctl --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  snapshot save /backup/etcd-snapshot-20260523.db

実行結果(JSON ログとともに snapshot saved メッセージが出力される):

{"level":"info","ts":"2026-05-23T10:00:00.000Z","caller":"snapshot/v3_snapshot.go:65","msg":"created temporary db file","path":"/backup/etcd-snapshot-20260523.db.part"}
{"level":"info","ts":"2026-05-23T10:00:00.001Z","caller":"snapshot/v3_snapshot.go:76","msg":"fetching snapshot","endpoint":"https://127.0.0.1:2379"}
{"level":"info","ts":"2026-05-23T10:00:00.123Z","logger":"client","caller":"v3@v3.6.6/maintenance.go:212","msg":"opened snapshot stream; downloading"}
{"level":"info","ts":"2026-05-23T10:00:00.456Z","caller":"snapshot/v3_snapshot.go:88","msg":"fetched snapshot","endpoint":"https://127.0.0.1:2379","size":"5.2 MB","took":"0.456 seconds"}
{"level":"info","ts":"2026-05-23T10:00:00.457Z","caller":"snapshot/v3_snapshot.go:96","msg":"saved","path":"/backup/etcd-snapshot-20260523.db"}
Snapshot saved at /backup/etcd-snapshot-20260523.db

最終行に Snapshot saved at /backup/etcd-snapshot-20260523.db と表示されれば取得成功です。サイズは本シリーズの環境で 5MB 程度(保存リソース数によって増減します)。

Step 3: etcdutl snapshot status による健全性確認

取得したスナップショットが「本当に restore できる状態か」を etcdutl snapshot status で確認します。このコマンドは v3.5 以降 etcdutl 側に集約されました(etcdctl snapshot status は v3.5 で deprecated・v3.6 で削除され、正式コマンドは etcdutl 側です)。--write-out=table オプションで人間に読みやすい表形式に整形します。

実行コマンド(k8s-cp-01 上・root):

# etcdutl snapshot status /backup/etcd-snapshot-20260523.db --write-out=table

実行結果(hash / revision / total-keys / total-size / version の 5 項目を確認):

+-----------+----------+------------+------------+---------+
|   HASH    | REVISION | TOTAL KEYS | TOTAL SIZE | VERSION |
+-----------+----------+------------+------------+---------+
| a1b2c3d4e5|     2456 |        738 |     5.2 MB |   3.6.0 |
+-----------+----------+------------+------------+---------+

各項目の意味を整理します。HASH はスナップショットの整合性ハッシュで、ファイル改ざんがないかを確認できます。REVISION は etcd が記録する内部リビジョン番号で、取得時点の世代を示します。TOTAL KEYS は保存されているキー数で、本シリーズの 5 ノード HA クラスタ + fanclub-api 程度なら数百〜千程度になります。TOTAL SIZE はスナップショットファイルのサイズ、VERSION は etcd のバージョン(本シリーズは 3.6.0)です。HASH や TOTAL KEYS が想定範囲から大きく外れる場合は、保存中にデータ破損が発生した可能性があるため、再取得を検討します。

Step 4: スナップショットファイルの属性確認

最後にファイル属性を確認します。サイズ・所有者・パーミッションが想定通りであることを ls -lh で確認します。

実行コマンド(k8s-cp-01 上・root):

# ls -lh /backup/etcd-snapshot-20260523.db

実行結果:

-rw------- 1 root root 5.2M May 23 10:00 /backup/etcd-snapshot-20260523.db

パーミッションが 600(root のみ読み書き可)になっていることを確認します。snapshot save は自動的にこのパーミッションでファイルを作成します。

本番ガードレール: 定期バックアップの自動化

本番環境では crontab で毎日深夜に snapshot save を実行し、直近 7 日分を保持する運用が一般的です。たとえば 0 2 * * * etcdctl ... snapshot save /backup/etcd-snapshot-$(date +\%Y\%m\%d).db のように設定し、別途古いファイルの削除ジョブを併設します。さらにスナップショットはローカルだけでなく、S3 や Azure Blob などの外部ストレージにも転送しておくのが本番運用の基本です。本回は手動で 1 回実施しますが、実際の運用では自動化を忘れずに設定してください。

やってみよう①: snapshot save → テスト Namespace 作成 → etcd restore で復旧確認

本回最大の演習です。etcd の「壊して直す」体験を通じて、snapshot save から restore までの一連の手順と Static Pod manifest 更新を確実に身につけます。シナリオは次の 7 ステップで進めます。

① snapshot save(前セクションで作成済み)
② テスト Namespace "restore-test" と ConfigMap を作成(後で消えるはずのリソース)
③ etcd Static Pod を停止(manifest を退避)
④ 旧データディレクトリをバックアップ(/var/lib/etcd を退避)
⑤ etcdutl snapshot restore で新しいデータディレクトリに復元
⑥ etcd.yaml の --data-dir と volumes.hostPath.path を更新(3 箇所すべて)
⑦ manifest を戻す → kubelet が etcd を再起動 → restore-test Namespace が復元されていることを確認

作業場所は k8s-cp-01(root 権限)と k8s-ops(kubectl 操作・developer)の 2 か所を行き来します。プロンプト記号(#$)で見分けてください。

本番運用警告②: etcd は Kubernetes クラスタの心臓部

本セクションの演習は学習用クラスタでのみ実施してください。本番クラスタでの snapshot restore は「最後の手段」であり、手順を誤るとクラスタ全損につながります。実機で行う前に必ず etcdutl snapshot status でスナップショットの健全性を確認し、旧データディレクトリは削除ではなくリネーム退避するなど、ロールバック経路を用意してから作業を開始するのが鉄則です。

Step 1: テスト用リソースを作成(後で消えるはずのリソース)

restore の効果を検証するため、snapshot save の後に作成したリソースを意図的に用意します。「restore したらこのリソースは消えているはず」という対照群です。本来は逆(save 前に作って、restore 後に存在することを確認する)パターンがわかりやすいので、本演習ではすでに前セクションで snapshot save 済みの状態を活用し、ここから新しいリソースを追加します。

実行コマンド(k8s-ops 上・developer):

$ kubectl create namespace restore-test
$ kubectl create configmap test-config --from-literal=key=after-snapshot -n restore-test
$ kubectl get configmap test-config -n restore-test

実行結果(作成確認):

namespace/restore-test created
configmap/test-config created
NAME          DATA   AGE
test-config   1      3s

この時点で restore-test Namespace と test-config ConfigMap が存在します。これらは snapshot save 時点(前セクション)には存在しなかったリソースです。単一 etcd であれば restore 後にこの 2 つが消えて snapshot 時点に巻き戻りますが、本回の 3 メンバー HA 環境では k8s-cp-01 1 台のみの restore のため quorum によってデータが上書きされ、これらは残って見えます(この HA 特有の挙動は Step 8 で実際に確認します)。本回の主眼はあくまで「単一 etcd restore の操作手順」を体に覚えさせることにあります。

Step 2: etcd Static Pod を停止

kubelet は /etc/kubernetes/manifests/ 配下に置かれた YAML を Static Pod として常に起動・監視します。逆に言えば、このディレクトリから manifest を取り除けば、kubelet は対応する Pod を自動的に停止します。mv で manifest を別の場所(/tmp/etcd-manifest-backup/)に退避するのが基本パターンです。

実行コマンド(k8s-cp-01 上・root):

# mkdir -p /tmp/etcd-manifest-backup
# mv /etc/kubernetes/manifests/etcd.yaml /tmp/etcd-manifest-backup/
# ls -la /etc/kubernetes/manifests/

実行結果(etcd.yaml がディレクトリから消えていることを確認):

total 16
-rw------- 1 root root 2415 May 22 14:00 kube-apiserver.yaml
-rw------- 1 root root 2107 May 22 14:00 kube-controller-manager.yaml
-rw------- 1 root root 1454 May 22 14:00 kube-scheduler.yaml

k8s-ops から kubectl で etcd Pod の状態を確認します。kubelet が manifest 不在を検知してから停止するまで数秒のラグがあります。

実行コマンド(k8s-ops 上・developer・etcd Pod が停止したか確認):

$ kubectl get pods -n kube-system | grep etcd

実行結果(etcd-k8s-cp-01 が一覧から消えるまで待つ・k8s-cp-02/03 のみ残る):

etcd-k8s-cp-02   1/1     Running   0          2d
etcd-k8s-cp-03   1/1     Running   0          2d

etcd-k8s-cp-01 がリストから消えました。k8s-cp-01 上の etcd が停止した状態です。etcd-k8s-cp-02etcd-k8s-cp-03 が引き続き Running なので、HA クラスタとしては 2/3 メンバーで quorum を維持し、API Server からの kubectl 操作は継続できます。

Step 3: 旧データディレクトリをバックアップ

etcd の現行データディレクトリ /var/lib/etcd をリネーム退避します。削除しない理由は、restore 後に問題が発生した場合のロールバック経路を残すためです。「壊れたら戻す」を常に確保しておくのが本番運用の基本姿勢です。

実行コマンド(k8s-cp-01 上・root):

# mv /var/lib/etcd /var/lib/etcd-backup-20260523
# ls -ld /var/lib/etcd*

実行結果:

drwx------ 3 root root 4096 May 21 12:00 /var/lib/etcd-backup-20260523

/var/lib/etcd は存在せず、退避した /var/lib/etcd-backup-20260523 のみが残っている状態です。

Step 4: etcdutl snapshot restore で復元

いよいよ restore 本体の実行です。etcdutl snapshot restore--data-dir オプションで新しいデータディレクトリ(/var/lib/etcd-restored)を指定します。「新しいディレクトリに復元」する設計にする理由は、Step 6 で確認する manifest 更新(3 箇所のパス変更)の必要性を可視化するためです。

実行コマンド(k8s-cp-01 上・root):

# etcdutl snapshot restore /backup/etcd-snapshot-20260523.db \
  --data-dir=/var/lib/etcd-restored

実行結果(restore 完了ログ):

2026-05-23T10:05:00Z	info	snapshot/v3_snapshot.go:248	restoring snapshot	{"path": "/backup/etcd-snapshot-20260523.db", "wal-dir": "/var/lib/etcd-restored/member/wal", "data-dir": "/var/lib/etcd-restored", "snap-dir": "/var/lib/etcd-restored/member/snap"}
2026-05-23T10:05:00Z	info	membership/store.go:141	Trimming membership information from the backend...
2026-05-23T10:05:01Z	info	snapshot/v3_snapshot.go:257	restored snapshot	{"path": "/backup/etcd-snapshot-20260523.db", "wal-dir": "/var/lib/etcd-restored/member/wal", "data-dir": "/var/lib/etcd-restored", "snap-dir": "/var/lib/etcd-restored/member/snap"}

restored snapshot ログが出力されれば restore 成功です。中間ログの Trimming membership information は、スナップショットに含まれていた旧メンバー情報を取り除く処理です。etcdutl はオフラインユーティリティなので、稼働中の etcd には何も影響しません。

復元先のディレクトリ構成を確認します。

実行コマンド(k8s-cp-01 上・root):

# ls -la /var/lib/etcd-restored/member/

実行結果:

total 16
drwx------ 4 root root 4096 May 23 10:05 .
drwx------ 3 root root 4096 May 23 10:05 ..
drwx------ 2 root root 4096 May 23 10:05 snap
drwx------ 2 root root 4096 May 23 10:05 wal

snap(スナップショット)と wal(Write-Ahead Log)の 2 ディレクトリが作成されています。これが etcd データの実体です。

Step 5: etcd.yaml の現状確認(3 箇所のパスを把握)

本回最大の落とし穴に取り掛かります。etcd.yaml の中で /var/lib/etcd を参照している箇所は 3 箇所あります。restore したデータディレクトリ(/var/lib/etcd-restored)を使うためには、この 3 箇所すべてを書き換える必要があります。まず現状を grep で確認します。

実行コマンド(k8s-cp-01 上・root・更新が必要な箇所を確認):

# grep -n 'data-dir\|hostPath\|/var/lib/etcd' /tmp/etcd-manifest-backup/etcd.yaml

実行結果(3 箇所が /var/lib/etcd を参照していることを確認):

24:    - --data-dir=/var/lib/etcd
52:      mountPath: /var/lib/etcd
63:      path: /var/lib/etcd

3 箇所の役割を整理します。これが理解できると、なぜ 3 箇所すべての更新が必須なのかが腑に落ちます。

行番号項目役割
24--data-diretcd プロセス起動時に「データディレクトリ」として認識するパス(コンテナ内視点)
52volumeMounts.mountPathコンテナ内のマウントポイント(--data-dir と一致する必要がある)
63volumes.hostPath.pathホスト側の実ディレクトリ(コンテナにマウントされる元)

3 箇所が同じパスを指していないと、restore は反映されません。たとえば --data-dir を新パス、volumes.hostPath.path を旧パスにすると、ホストの「空(または別世代)の旧ディレクトリ」がコンテナの「新パス」にマウントされる構造になり、etcd は restore 内容を一切認識しません。

Step 6: sed で 3 箇所を一括置換

3 箇所の書き換えは手作業(vi 編集)でも可能ですが、漏れリスクを排除するため sed -i で一括置換します。-i は in-place 編集(元ファイル直接更新)です。

実行コマンド(k8s-cp-01 上・root):

# sed -i 's|/var/lib/etcd|/var/lib/etcd-restored|g' /tmp/etcd-manifest-backup/etcd.yaml
# grep -n 'data-dir\|hostPath\|/var/lib/etcd' /tmp/etcd-manifest-backup/etcd.yaml

実行結果(3 箇所すべてが /var/lib/etcd-restored に更新されていることを確認):

24:    - --data-dir=/var/lib/etcd-restored
52:      mountPath: /var/lib/etcd-restored
63:      path: /var/lib/etcd-restored

3 行とも新しいパスに更新されました。この 3 箇所一致が restore 成功の必須条件です。「grep で見て 3 行揃ったらゴーサイン」という習慣を身につけてください。CKA 試験本番でも、この grep 確認が「焦って 1 箇所だけ直してしまうミス」の最大の防御策になります。

もう一段だけ補足します。sed -i 's|/var/lib/etcd|/var/lib/etcd-restored|g' のように区切り文字に |(パイプ)を使っているのは、置換対象のパスにスラッシュ(/)が含まれるからです。s/.../.../ 形式だとパス内のスラッシュをエスケープする必要があり、視認性が落ちます。区切り文字は sed が許す任意の 1 文字に変更可能で、パスや URL を扱う場面では |# がよく使われます。本番運用でも覚えておくと打鍵数を減らせる小技です。

Step 7: manifest を戻して etcd を再起動

更新済みの etcd.yaml を /etc/kubernetes/manifests/ に戻します。kubelet は manifest 出現を検知し、自動的に新しい設定で etcd Static Pod を起動します。

実行コマンド(k8s-cp-01 上・root):

# cp /tmp/etcd-manifest-backup/etcd.yaml /etc/kubernetes/manifests/etcd.yaml
# ls -la /etc/kubernetes/manifests/etcd.yaml

実行結果:

-rw------- 1 root root 2456 May 23 10:08 /etc/kubernetes/manifests/etcd.yaml

k8s-ops から etcd Pod の状態を watch で確認します。-w オプションは状態変化を継続表示するモードです。

実行コマンド(k8s-ops 上・developer・etcd-k8s-cp-01 が起動するまで watch):

$ kubectl get pods -n kube-system -w | grep etcd-k8s-cp-01

実行結果(Pending → ContainerCreating → Running と遷移):

etcd-k8s-cp-01   0/1     Pending             0     0s
etcd-k8s-cp-01   0/1     ContainerCreating   0     1s
etcd-k8s-cp-01   1/1     Running             0     8s

Running になれば再起動成功です。Ctrl+C で watch を停止します。

Step 8: restore が成功したことを確認

ここで restore-test Namespace の状態を確認します。本回は 3 メンバー HA クラスタの k8s-cp-01「1 台のみ」を restore したため、挙動には注意が必要です。

実行コマンド(k8s-ops 上・developer):

$ kubectl get namespace restore-test

実行結果(3 メンバー HA では restore-test は残る):

NAME           STATUS   AGE
restore-test   Active   5m

restore-test は Active のまま残ります。これは本回冒頭の本番運用警告①で述べたとおりの挙動です。3 メンバー HA で k8s-cp-01 だけを restore して起動しても、k8s-cp-02・k8s-cp-03 が quorum(過半数)を維持しているため、既定の kubeconfig(k8s-lb 経由でラウンドロビン)で kubectl が返すデータは「多数派である k8s-cp-02/03 側の最新状態」になります。そのため snapshot 時点に存在しなかった restore-test は消えずに残って見えます。逆に言えば「単一メンバーだけの restore はデータ面では quorum に上書きされる」ことが実機で確認できるわけです。本演習で確実に習得すべきは「① etcd 停止 → ② 旧データ退避 → ③ etcdutl で復元 → ④ manifest を 3 箇所整える → ⑤ 起動して 3 メンバー healthy を確認」という単一 etcd restore の操作手順です。restore したデータ内容をクラスタ全体に反映させる「データ面のロールバック」を実体験するには、全 3 メンバーを同時に restore する HA 全 restore(第15回)が必要になります。

続いて endpoint health --cluster で各メンバーの健全性を確認します。ここで重要な注意点があります。本演習のように 3 メンバー HA が稼働したまま k8s-cp-01「1 台だけ」を etcdutl snapshot restore すると、復元したデータには新しいクラスタ ID が割り当てられます。稼働中の cp-02/03(元のクラスタ ID)とはクラスタ ID が異なるため、k8s-cp-01 の etcd は既存 quorum へ合流できず、孤立した単一メンバークラスタとして起動します。

実行コマンド(k8s-cp-01 上・root):

# etcdctl --endpoints=https://192.168.1.125:2379,https://192.168.1.126:2379,https://192.168.1.127:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  endpoint health --cluster

実行結果(k8s-cp-01 はクラスタ視点で unhealthy・quorum は cp-02/03 の 2/3 で継続):

https://192.168.1.126:2379 is healthy: successfully committed proposal: took = 4.5ms

https://192.168.1.127:2379
is healthy: successfully committed proposal: took = 4.2ms
https://192.168.1.125:2379
is unhealthy: failed to commit proposal: context deadline exceeded Error: unhealthy cluster

k8s-cp-01 の etcd ログには request cluster ID mismatch が繰り返し記録されます。これは復元した単一メンバー(新クラスタ ID)と稼働中の cp-02/03(元のクラスタ ID)が別クラスタとして扱われていることを示します。bare な snapshot restore は「全メンバー停止 → 全 restore → –initial-cluster 再設定」というクラスタ全体の復旧手順向けであり、稼働中の quorum に 1 台だけ戻す用途には使えません。単一メンバーを正しく戻すには、稼働クラスタから当該メンバーを etcdctl member remove で外し、etcdctl member add で追加し直してから、空のデータディレクトリと --initial-cluster-state=existing で etcd を起動します(leader から自動でデータが同期されます)。この単一メンバー復旧と HA 全 restore の手順は第15回で扱います。

このように、データ面では先ほど確認したとおり restore-test が Active のまま残り、メンバー構成面でも k8s-cp-01 は孤立して quorum に戻りません。「3 メンバー HA が健全に動いている最中の単一メンバー bare restore は、狙った復旧にはならない」と理解しておきましょう。本回で確実に習得すべきは Steps 1〜7 の機構(停止 → 旧データ退避 → etcdutl restore → manifest 3 箇所更新 → 再起動)であり、これは単一 etcd クラスタや CKA 試験で問われる「クラスタ全体の restore」でそのまま通用します。

fanclub-api が引き続き稼働していることも、ついでに確認しておきます。

実行コマンド(k8s-ops 上・developer):

$ kubectl get pods -n fanclub

実行結果(snapshot save 時点の Pod 構成で稼働継続):

NAME                                      READY   STATUS    RESTARTS   AGE
fanclub-api-fanclub-api-bbbfdf9bf-abcde   1/1     Running   0          2d
fanclub-api-fanclub-api-bbbfdf9bf-fghij   1/1     Running   0          2d

fanclub-api Pod が snapshot save 時点の構成で稼働しています。restore で巻き戻ったのは etcd 内のメタデータ(Namespace・ConfigMap など)であり、Workload Node 上で実際に走っている Pod プロセス自体は影響を受けません。これが「etcd restore = クラスタの記憶を巻き戻す」操作の本質です。

もしも restore 時に「Pod 定義そのものが etcd から消えてしまった」場合は、kube-controller-manager が状態の不整合を検出して Pod の再作成や削除を進めます。たとえば snapshot save 時点に存在しなかった Pod が現在走っていれば、controller の調停によって停止されることもあります。本回の演習では restore 直後にこのような変動は発生しませんでしたが、本番運用では「restore 後 5〜10 分は controller の動きを観察する」ことが基本になります。kubectl get events --sort-by=.metadata.creationTimestamp -A を流し続けて、想定外の Pod 削除や再作成が走っていないかを確認します。

Static Pod manifest 更新の落とし穴(data-dir / volumes 更新忘れ)

やってみよう① の Step 5〜6 で扱った「3 箇所更新」は、CKA 試験で最も頻発する落とし穴です。本セクションでは、よくある失敗パターンを 2 つ取り上げ、それぞれの症状と対処を整理します。試験本番で「あれ、restore が反映されていない」と慌てたときの診断手順としても活用できます。

落とし穴① –data-dir だけ更新し volumes.hostPath.path を忘れるパターン

最も頻発する失敗パターンです。--data-dirvolumeMounts.mountPath は更新したが、volumes.hostPath.path を忘れる、というケースです。下記の YAML 抜粋が「失敗例」です。

spec:
  containers:
  - command:
    - etcd
    - --data-dir=/var/lib/etcd-restored   # ここは更新した
    name: etcd
    volumeMounts:
    - mountPath: /var/lib/etcd-restored   # ここも更新した
      name: etcd-data
  volumes:
  - name: etcd-data
    hostPath:
      path: /var/lib/etcd                 # ← ここを忘れた(NG)
      type: DirectoryOrCreate

このとき何が起きるかを構造で説明します。volumes.hostPath.path はホスト側の実ディレクトリ、volumeMounts.mountPath はコンテナ内のマウントポイントです。両者は「ホストの何を、コンテナのどこに見せるか」を決めるペアです。失敗例の構造は次のようになります。

etcd snapshot save → restore フロー(Static Pod manifest 3 箇所整合)
etcd snapshot save → restore フロー(Static Pod manifest 3 箇所整合)

ホストの旧ディレクトリ(空)がコンテナにマウントされ、etcd はデータがない状態で起動します。新規 etcd クラスタとして初期化を試みるため、API Server からは「クラスタが空っぽになった」ように見えます。深刻なケースでは Namespace 一覧すら空に見える事態となり、本番では大規模障害につながります。

この症状の厄介な点は、etcd Static Pod 自体は Running 状態で起動してしまうことです。kubectl get pods -n kube-system | grep etcd でも問題なく見えるため、表面的には「restore が成功した」と勘違いしやすいのです。「restore したのにリソースが見えない」と気づいて初めて、Static Pod manifest の volumes.hostPath.path 更新忘れを疑う、というケースが現場で繰り返し発生しています。

落とし穴② data-dir を旧パス /var/lib/etcd のままにするパターン

逆に、etcdutl snapshot restore/var/lib/etcd-restored に出力したが、etcd.yaml 側は 3 箇所すべて旧パス /var/lib/etcd のまま放置するパターンです。

Step 3 で /var/lib/etcd は退避済みなので、ホスト側にこのディレクトリは存在しません。volumes.hostPath.type: DirectoryOrCreate が指定されていると、kubelet は自動的に空ディレクトリ /var/lib/etcd を作成してマウントします。その結果、etcd は新規クラスタとして初期化されます。restore したデータは /var/lib/etcd-restored に放置されたまま、一切使われません。落とし穴① と症状はそっくりですが、原因は逆方向です。

このパターンは「etcdutl snapshot restore のオプションだけ覚えていて、manifest 編集を忘れる」初学者が陥りやすい失敗です。本回の演習で「restore コマンドそれ自体は前段で、本番は manifest 編集」という配分を実機で確認しておくことが、本番運用で同じ轍を踏まない最大の対策になります。

本番運用警告③: Static Pod manifest の volumes.hostPath.path 更新忘れ

--data-dir だけ変更し volumes.hostPath.path を更新し忘れると、ホストの旧ディレクトリがコンテナにマウントされ restore が反映されません。本番環境での発生は深刻で、API Server からはクラスタが空に見える事態を招きます。CKA 試験でも頻出の落とし穴です。grep -E '--data-dir|mountPath|hostPath' /etc/kubernetes/manifests/etcd.yaml で 3 箇所すべてが同じパスを指していることを必ず確認してください。

試験本番用チェックリスト

CKA 試験本番で restore タスクが出題されたときの確認手順を、チェックリスト形式で整理します。grep 1 回で 3 箇所すべてを並べて目視確認するのが最も確実です。

etcd.yaml 更新チェックリスト
□ containers[0].command の --data-dir=<新ディレクトリ>
□ containers[0].volumeMounts[etcd-data].mountPath=<新ディレクトリ>
□ volumes[etcd-data].hostPath.path=<新ディレクトリ>
(3 箇所すべて同じパスを指定する)

確認用のワンライナーを暗記しておくと役立ちます。

実行コマンド(k8s-cp-01 上・root・3 箇所一致確認):

# grep -E 'data-dir|mountPath|hostPath|/var/lib' /etc/kubernetes/manifests/etcd.yaml

実行結果(3 箇所すべてが同じパスを指していることを確認):

    - --data-dir=/var/lib/etcd-restored
      mountPath: /var/lib/etcd-restored
      path: /var/lib/etcd-restored

vi で個別に編集する場合は更新漏れのリスクが残ります。やってみよう①で使った sed -i 's|旧パス|新パス|g' による一括置換のほうが安全です。本番運用でも、Static Pod manifest の機械的な書き換えは sed や yq などのツールを使う運用が広まっています。

やってみよう②: kubectl drain と uncordon — ノードメンテナンスの基本

本回 2 つ目の演習です。ノードを安全にメンテナンスする手順「drain → 作業 → uncordon」を実機で体験します。OS パッチ適用、カーネル更新、ハードウェア交換など、ノードを一時的にクラスタから切り離す場面は本番運用で頻発します。さらに第6回の kubeadm upgrade ではこの drain/uncordon が中核手段になります。本回で確実に手順を身につけてください。

シナリオの全体像は次の通りです。

① drain 前のノード状態を確認(kubectl get nodes / kubectl get pods -o wide)
② k8s-wl-02 を drain する
③ fanclub-api Pod が k8s-wl-01 に移動したことを確認
④ 仮想的なメンテナンス(実機では何もしない)
⑤ k8s-wl-02 を uncordon する
⑥ ノード状態が Ready に戻ることを確認

作業場所は k8s-ops(developer・kubectl 操作)です。drain/uncordon はすべてクライアント側の kubectl コマンドで完結します。

Step 1: drain 前の状態確認

まず drain 前の状態をスナップショットとして記録します。5 ノードすべてが Ready であること、fanclub-api Pod が wl-01・wl-02 に分散していることを確認します。

実行コマンド(k8s-ops 上・developer):

$ kubectl get nodes

実行結果(5 ノード全員 Ready):

NAME         STATUS   ROLES           AGE   VERSION
k8s-cp-01    Ready    control-plane   5d    v1.35.5
k8s-cp-02    Ready    control-plane   4d    v1.35.5
k8s-cp-03    Ready    control-plane   4d    v1.35.5
k8s-wl-01    Ready    <none>          4d    v1.35.5
k8s-wl-02    Ready    <none>          4d    v1.35.5

続いて fanclub-api Pod の配置ノードを -o wide で確認します。

実行コマンド(k8s-ops 上・developer):

$ kubectl get pods -n fanclub -o wide

実行結果(Pod が wl-01 / wl-02 に分散していることを確認):

NAME                                    READY   STATUS    RESTARTS   AGE   NODE
fanclub-api-fanclub-api-bbbfdf9bf-abcde   1/1     Running   0          2d    k8s-wl-01
fanclub-api-fanclub-api-bbbfdf9bf-fghij   1/1     Running   0          2d    k8s-wl-02

backend の 2 Pod が wl-01・wl-02 に 1 つずつ分散配置されています。frontend と fanclub-db(PostgreSQL)は第4回時点の Helm chart には含まれておらず未デプロイのため、この時点で fanclub Namespace に存在するのは backend の 2 Pod のみです(fanclub-db は第12回で StatefulSet としてデプロイします)。drain 対象を k8s-wl-02 にすると、退避対象は wl-02 上の backend Pod 1 つに限られ、影響範囲を把握しやすくなります。

Step 2: dry-run で drain 影響範囲を事前確認

本番運用の鉄則として、drain を実際に走らせる前に --dry-run=client で影響範囲を確認します。退避される Pod の一覧、スキップされる DaemonSet が事前に把握できます。

実行コマンド(k8s-ops 上・developer・dry-run):

$ kubectl drain k8s-wl-02 --ignore-daemonsets --delete-emptydir-data --dry-run=client

実行結果(退避される Pod の一覧が表示される・実際の退避は発生しない):

node/k8s-wl-02 cordoned (dry run)
WARNING: ignoring DaemonSet-managed Pods: kube-system/calico-node-xxxxx, kube-system/kube-proxy-yyyyy
evicting pod fanclub/fanclub-api-fanclub-api-bbbfdf9bf-fghij (dry run)
node/k8s-wl-02 drained (dry run)

影響範囲が明確になりました。退避対象は fanclub-api-fanclub-api の Pod 1 つ、スキップ対象は Calico CNI(calico-node)と kube-proxy の DaemonSet です。--ignore-daemonsets がない場合、これらの DaemonSet が原因で drain がエラー停止します。

Step 3: drain を実行

dry-run の結果に問題がなければ本番実行します。--dry-run=client を外すだけです。

実行コマンド(k8s-ops 上・developer・本番実行):

$ kubectl drain k8s-wl-02 --ignore-daemonsets --delete-emptydir-data

実行結果(cordon → DaemonSet 警告 → evict → drained と段階表示):

node/k8s-wl-02 cordoned
WARNING: ignoring DaemonSet-managed Pods: kube-system/calico-node-xxxxx, kube-system/kube-proxy-yyyyy
evicting pod fanclub/fanclub-api-fanclub-api-bbbfdf9bf-fghij
pod/fanclub-api-fanclub-api-bbbfdf9bf-fghij evicted
node/k8s-wl-02 drained

drain は内部的に「cordon(スケジューリング無効化)→ evict(既存 Pod の退避)」の 2 段階で進行します。各 Pod は PodDisruptionBudget の制約を尊重しながら順次退避されます。

Step 4: drain 後の状態確認

drain 後のノード状態を確認します。k8s-wl-02 が Ready,SchedulingDisabled 表示になっていれば cordon が効いた証拠です。カンマ区切りで複数ステータスが表示されるのが特徴です。

実行コマンド(k8s-ops 上・developer):

$ kubectl get nodes

実行結果(k8s-wl-02 が SchedulingDisabled になっていることを確認):

NAME         STATUS                     ROLES           AGE   VERSION
k8s-cp-01    Ready                      control-plane   5d    v1.35.5
k8s-cp-02    Ready                      control-plane   4d    v1.35.5
k8s-cp-03    Ready                      control-plane   4d    v1.35.5
k8s-wl-01    Ready                      <none>          4d    v1.35.5
k8s-wl-02    Ready,SchedulingDisabled   <none>          4d    v1.35.5

Pod の移動先も確認します。fanclub-api-fanclub-api の Pod が k8s-wl-01 に移動していれば成功です。

実行コマンド(k8s-ops 上・developer):

$ kubectl get pods -n fanclub -o wide

実行結果(fanclub-api-fanclub-api Pod が k8s-wl-01 に移動):

NAME                                      READY   STATUS    RESTARTS   AGE   NODE
fanclub-api-fanclub-api-bbbfdf9bf-abcde   1/1     Running   0          2d    k8s-wl-01
fanclub-api-fanclub-api-bbbfdf9bf-pqrst   1/1     Running   0          25s   k8s-wl-01

fanclub-api-fanclub-api の Pod 2 つはいずれも k8s-wl-01 に集約されました。新しく起動した Pod(...-pqrst)は AGE が 25s と新しく、これが Step 3 の evict 後に別ノードでスケジュールし直された Pod です。Deployment コントローラーが「Pod 不足」を検知して新規 Pod を作成し、スケジューラーが SchedulingDisabled でない k8s-wl-01 に配置した結果です。

このタイミングで OS パッチ適用やカーネル更新といったメンテナンス作業を行います。本演習では何もせず、すぐに uncordon に進みます。

本番の運用シーンを想像してみます。たとえば AlmaLinux のセキュリティパッチが公開された日、ノード単位で順番に drain → dnf update → 再起動 → uncordon を繰り返す手順を取ります。3 台の Workload Node がある場合、1 台ずつ drain してパッチを当てれば、残り 2 台でワークロードを支えながらメンテナンスを進められます。これがローリングメンテナンスの基本パターンであり、第6回の kubeadm upgrade でも同じ流れを踏襲します。drain → 作業 → uncordon の 3 ステップは、本番運用で最も頻繁に使うクラスタ操作の一つです。

Step 5: uncordon でノードを復帰

メンテナンス完了後、kubectl uncordon でノードを復帰させます。これだけでスケジューリング対象に戻ります。

実行コマンド(k8s-ops 上・developer):

$ kubectl uncordon k8s-wl-02

実行結果:

node/k8s-wl-02 uncordoned

ノード状態を確認します。

実行コマンド(k8s-ops 上・developer):

$ kubectl get nodes

実行結果(k8s-wl-02 が Ready のみに戻る):

NAME         STATUS   ROLES           AGE   VERSION
k8s-cp-01    Ready    control-plane   5d    v1.35.5
k8s-cp-02    Ready    control-plane   4d    v1.35.5
k8s-cp-03    Ready    control-plane   4d    v1.35.5
k8s-wl-01    Ready    <none>          4d    v1.35.5
k8s-wl-02    Ready    <none>          4d    v1.35.5

uncordon 後、Kubernetes のスケジューラーはすぐには既存 Pod を k8s-wl-02 に戻しません。不必要な Pod の再スケジューリングを避ける設計だからです。k8s-wl-01 の負荷が高くなった場合や、新しい Pod がスケジュールされる場面で k8s-wl-02 が配置候補に入ります。手動で再分散したい場合は Deployment の kubectl rollout restart を使う方法もありますが、本回では扱いません。

Pod の再分散を厳密にコントロールしたい場合は、第7回で扱う taint / toleration や nodeAffinity を使い、ノード復帰時に特定の Pod を戻す設計も可能です。さらに本格的な再分散には Descheduler という OSS(CNCF Incubating プロジェクト)を使う方法もありますが、本シリーズでは標準機能のみで完結する範囲に絞り、Descheduler は扱いません。「uncordon は復帰宣言であって再分散ではない」という性質を理解しておくのが、本回の到達点です。

drain / cordon / uncordon の比較

3 つのコマンドの違いを表で整理します。「cordon は予防、drain は退避、uncordon は復帰」と覚えてください。

コマンド説明新 Pod の配置既存 Pod の扱い
kubectl cordon <node>スケジューリングのみ無効化禁止そのまま稼働
kubectl drain <node>cordon + 既存 Pod の退避禁止他ノードへ退避
kubectl uncordon <node>スケジューリング再有効化再開変更なし

本番運用警告④: drain と kubectl delete node を混同しない

kubectl drain はノード上のワークロードを退避するだけで、ノード自体はクラスタに残ります。一方 kubectl delete node はクラスタから完全削除する操作で、戻すには再度 kubeadm reset + kubeadm join が必要になります。本回では drain / uncordon のみを扱い、kubectl delete node は対象外としています。誤って delete node してしまうと、ノードを再 join するまでワークロードを戻せません。本番では実行前にコマンドを 2 度読み直す習慣が事故防止につながります。

第1部完走宣言 — 5 回完走・第2部「ワークロード管理」へ

第5回まで完走したことで、第2巻第1部「クラスタ構築」(全 5 回)が完了しました。第1回でゼロから出発し、第5回までで kubeadm HA クラスタを自力で構築し、運用に必要な最低限のスキル(バックアップ・リストア・ノードメンテナンス)を習得しました。第1部の歩みを振り返ります。

第1部達成スキル一覧

習得回スキル達成の証
第1回CKA 試験形式の理解 + kubeadm の位置づけ把握kubeadm vs RKE2 / k0s / OKD の使い分けを説明できる
第2回kubeadm シングルノードクラスタ起動 + alma-proxy whitelistkubectl get nodes で Ready 確認
第3回kubeadm HA 設計 + HAProxy LB 構成HAProxy stats ページで全 backend UP 確認
第4回kubeadm HA クラスタ構築(CP×3 + WL×2)+ fanclub-api 移行5 ノード全 Ready・fanclub-api 稼働
第5回etcd backup/restore + ノード drain/uncordonsnapshot save + restore 自力実施・drain/uncordon 成功

5 回かけて、検証環境のゼロ状態から「本番運用に耐えうる kubeadm HA クラスタ」を自分の手で組み上げました。kubeadm init、HAProxy LB、CP×3 + WL×2 構成、etcd backup/restore、ノード drain/uncordon — これらは CKA D1(Cluster Architecture, Installation and Configuration)の主要トピックを網羅しており、CKA 試験対策としても土台が整いました。

第1部の 5 回は、いずれも単発の操作ではなく「前回の成果物を土台に積み重ねる」構造でした。第1回で kubeadm という設計思想を理解し、第2回でシングルノードを起動し、第3回で HAProxy を本番設計に作り込み、第4回で 5 ノード HA を完成させ、第5回で運用基盤(バックアップ・ノード操作)を整える — この積み重ねは、本番の Kubernetes 構築プロジェクトでも踏襲する順序です。教科書で学んだ流れがそのまま現場で使えるのは、本シリーズが「実機 + kubeadm 公式手順」に軸足を置いて設計されているからです。

本回で「壊して直す」を体験したことで、CKA 試験本番で etcd restore タスクが出題されても、手順を頭の中で再生できる状態になっているはずです。試験では時間制限の中で焦りやすい操作ですが、本回で踏んだ 7 ステップ(snapshot save → テストリソース作成 → etcd 停止 → 旧データ退避 → restore → etcd.yaml 3 箇所更新 → manifest 復元)を 1 度通して経験していれば、試験会場でも落ち着いて手を動かせます。

第1部完走チェックリスト

第2部へ進む前に、第1部の到達度を自己チェックします。すべて埋まれば第2部への準備が完了です。

第1部完走チェックリスト
□ kubeadm HA クラスタ(CP×3 + WL×2)が稼働している
□ fanclub-api が kubeadm HA クラスタ上で稼働している
□ etcdutl / etcdctl がインストールされ、endpoint health が成功する
□ /backup/etcd-snapshot-YYYYMMDD.db が取得できた
□ snapshot restore + etcd.yaml 更新(3 箇所)の手順を自力で実施できた
□ kubectl drain / uncordon でノードのメンテナンスができた

第2部「ワークロード管理」予告(第6〜8回)

第2部(第6〜8回)では、構築したクラスタを「アップグレード」し「ワークロードを管理」するスキルを習得します。各回の概要は次の通りです。

  • 第6回: kubeadm upgrade plan / apply でクラスタをバージョンアップ + ノード drain / cordon でローリングアップグレード
  • 第7回: taint / toleration / nodeSelector / affinity で Pod を意図したノードに配置
  • 第8回: HPA で自動スケール + ResourceQuota / LimitRange でリソース管理

第1部が「クラスタを作る」、第2部が「クラスタを使う」と言い換えられます。第5回で習得した drain / uncordon は、第6回の kubeadm upgrade で中核となる手段です。第1部の学びがそのまま第2部の土台になります。

第2部以降、操作対象は「クラスタそのもの」から「クラスタの上で動くワークロード」に重心が移ります。第6回の kubeadm upgrade はその過渡期にあたる回で、クラスタ自体を新しいバージョンに更新しつつ、ワークロード(fanclub-api)を止めずに移行させる手順を扱います。第5回の drain / uncordon と etcd backup / restore は、その upgrade 作業中に「もしものとき」の安全網としても機能します。クラスタを止めずに上げる、その途中で何かあっても戻せる — この二つを両立させるのが本番運用の基本姿勢です。第1部の最終回として、第5回はその基礎をひと通り体験する回になりました。

まとめと次回予告

第5回で習得したことを 5 点にまとめます。

  • etcd はクラスタの全状態を保持するキーバリューストア。etcd のバックアップは「保険」ではなく「必須運用タスク」である
  • etcdctl snapshot save でスナップショット取得 → etcdutl snapshot status で健全性確認(v3.5.0 以降の正式ツールは etcdutl
  • restore は「① etcd 停止 → ② 旧データ退避 → ③ etcdutl snapshot restore → ④ etcd.yaml 3 箇所更新 → ⑤ manifest 復元」の 5 ステップ
  • etcd.yaml の更新は --data-dir / volumeMounts.mountPath / volumes.hostPath.path の 3 箇所すべてが必須
  • kubectl drain --ignore-daemonsets --delete-emptydir-data でワークロードを安全に退避し、kubectl uncordon で復帰させる

第6回では、稼働中の kubeadm HA クラスタをバージョンアップします。kubeadm upgrade plan でアップグレード可能なバージョンを確認し、kubeadm upgrade apply で Control Plane Node を更新します。さらに各 Workload Node を drain → kubelet / kubectl アップグレード → uncordon の手順で安全にローリングアップグレードを実施します。クラスタを止めずにアップグレードする技術は CKA D1 の重要テーマであり、現場でも頻繁に求められるスキルです。第1部で構築した基盤を守り続けるための「定期メンテナンス」の第一歩が第6回です。

現場ヒヤリハット

本回の作業で実際に起きやすいトラブルを 2 件紹介します。いずれも etcd 運用とノードメンテナンスの現場で頻発するものです。対処法と教訓をセットで頭に入れておくと、同じ落とし穴を避けられます。

ヒヤリハット① etcd restore 後の volumes.hostPath.path 更新忘れ

状況: 深夜の障害対応で急いで etcdutl snapshot restore /backup/latest.db --data-dir=/var/lib/etcd-restored を実行し、etcd.yaml--data-dir だけを更新してクラスタを復旧させたつもりでした。翌朝確認すると API Server が「etcd データなし」状態で起動しており、クラスタが空のように見えました。kubectl get namespace では default など kubeadm デフォルトの Namespace は表示されるものの、本来あるはずの fanclub・monitoring・argocd 等の業務 Namespace がすべて消えていました(kube-system は kubelet が Static Pod として再構築するため表示は残るが、Pod のスケジュール情報やラベルは失われた状態)。

原因: etcd.yamlvolumes.hostPath.path が旧パス /var/lib/etcd のままだったため、コンテナには空の旧ディレクトリがマウントされ、restore した /var/lib/etcd-restored の中身が一切使われていませんでした。深夜の対応では「--data-dir さえ更新すれば良いだろう」と思い込み、3 箇所一致の確認を怠ったのが直接の原因です。

対処: etcd.yamlvolumes.hostPath.path/var/lib/etcd-restored に更新し、etcd を再起動して復旧しました。再起動後、kubectl get namespace で正常なリソース一覧が戻ったことを確認しました。

教訓: restore 後は必ず grep -E 'data-dir|mountPath|hostPath' etcd.yaml を実行し、3 箇所すべてのパスが一致しているかを確認してから manifest を戻します。本回の sed 一括置換は、この教訓を直接反映した手順です(sed -i "s|/var/lib/etcd|/var/lib/etcd-restored|g" /etc/kubernetes/manifests/etcd.yaml で 3 箇所同時に置換できる・置換後は必ず grep -E "data-dir|mountPath|hostPath" etcd.yaml で確認)。「3 箇所のうちどれか 1 つでも忘れたら restore は失敗する」を体に刻んでください。

ヒヤリハット② drain と local-path StorageClass の PVC が衝突するパターン(第12回 fanclub-db デプロイ後に発生)

本回時点では fanclub-db StatefulSet はまだデプロイされていません(第12回で初めて Longhorn 上に作成)が、PVC を伴う StatefulSet を drain したときに陥る典型シナリオを先に共有しておきます。第12回以降の演習や本番運用で必ず通る論点です。

状況: 検証中、PostgreSQL StatefulSet(fanclub-db)が稼働中のノード k8s-wl-01 を drain したところ、Pod が k8s-wl-02 でも k8s-wl-01 でも Pending のまま起動しません。drain は完了したのに、本来移動するはずの fanclub-db-0 が宙ぶらりんになる事態でした。

原因: fanclub-db が PVC(local-path-provisioner による local-path StorageClass)を使っており、PVC は特定のノード(k8s-wl-01)のローカルディスクにバインドされていました。kubectl describe pod fanclub-db-0 の Events に had volume node affinity conflict と出ており、PV の node affinity がスケジューリングを拒否していたのです。drain によって元ノードが SchedulingDisabled になり、PVC を持つ StatefulSet Pod のスケジューリングが別ノードでは構造的に不可能になりました。

対処: kubectl uncordon k8s-wl-01 で元ノードを復帰させ、Pod を k8s-wl-01 で再起動して復旧しました。PVC を持つ StatefulSet を drain で動かしたい場合は、事前に PV のレプリケーション基盤を整えるか、対象ノードを drain しない運用ルールが必要です。

教訓: ローカルストレージ(local-path-provisioner)を使った StatefulSet は、データが保存されているノードでしか起動できません。本番環境では Longhorn のような「複数ノードにレプリカを持つ分散ストレージ」を使うことで、ノード障害時でも Pod を別ノードで起動できるようになります。第12回で local-path-provisioner から Longhorn への移行を扱います。

理解度チェック

第5回の理解度を ○× 形式の 8 問で確認します。まず問題を読み、自分なりに答えを出してから解説を読んでください。

  • 問 1: etcdctl snapshot save コマンドで etcd のスナップショットを取得できる
  • 問 2: etcd v3.6 では snapshot restore に etcdutl を使うのが正式ツールである
  • 問 3: etcdutl snapshot restore を実行するだけで etcd が自動的に再起動する
  • 問 4: etcd.yaml--data-dir だけを更新すれば restore が正しく反映される
  • 問 5: kubectl cordon はスケジューリングを無効化するが、既存 Pod はそのまま稼働する
  • 問 6: kubectl drain は DaemonSet 管理下の Pod も自動的に退避する
  • 問 7: HA クラスタ(3 メンバー etcd)では、1 ノードだけ snapshot restore すれば他のメンバーにも反映される
  • 問 8: kubectl uncordon 実行後、直ちにすべての Pod が均等に再配置される

問 1: ○ — etcdctl snapshot save は v3.6 でも snapshot save の正式コマンドです。restore は etcdutl に移行しましたが、save 側は引き続き etcdctl の役割として残っています。「save は etcdctl、restore は etcdutl」と覚えてください。

問 2: ○ — etcd v3.5.0 から etcdutl snapshot restore が正式コマンドになりました。etcdctl snapshot restore は etcd v3.6 で削除されたため、本シリーズの etcd v3.6.6 では etcdutl snapshot restore を使用します。kubernetes.io 公式ドキュメントも etcdutl 表記です。

問 3: × — etcdutl snapshot restore は新しいデータディレクトリにスナップショットを展開するだけで、etcd プロセスは起動しません。etcd.yaml--data-dir / volumeMounts.mountPath / volumes.hostPath.path を更新し、manifest を /etc/kubernetes/manifests/ に戻すことで kubelet が再起動します。

問 4: × — etcd.yaml 内で /var/lib/etcd を参照する箇所は 3 つあります。containers[0].command --data-dircontainers[0].volumeMounts.mountPathvolumes.hostPath.path です。3 箇所すべてを同じパスに揃えないと、ホストの旧ディレクトリがコンテナにマウントされ restore が反映されません。本回最大の落とし穴です。

問 5: ○ — kubectl cordon はスケジューリング無効化のみを行う「予防」操作です。既存 Pod は影響を受けず、そのまま稼働を継続します。新規 Pod がそのノードに配置されない状態を作るだけです。一方 kubectl drain は cordon に加えて既存 Pod の退避も行います。

問 6: × — DaemonSet 管理下の Pod(Calico CNI、kube-proxy など)は drain で退避できません。これらは「全ノードで 1 つずつ稼働する」性質上、退避先がないからです。--ignore-daemonsets フラグで「DaemonSet Pod は退避対象外にする」ことを明示する必要があります。フラグがないと drain はエラーで停止します。

問 7: × — 3 メンバー HA で 1 ノードだけ restore すると、quorum で他の 2 メンバーとの間で多数決が行われ、restore 内容が上書きされる可能性があります。本番の HA 全 restore は「全 3 メンバーで同時に restore + --initial-cluster 再設定」が必須です。本回演習は単一 etcd シナリオで実施しました。HA 全 restore は第15回で扱います。

問 8: × — kubectl uncordon はスケジューリングを再開するだけで、既存 Pod を強制的に再配置することはありません。不必要な Pod の移動を避けるためです。他ノードの負荷が高まったり、新規 Pod が作成された場合に、復帰したノードが配置候補に入ります。手動で再分散したい場合は kubectl rollout restart deployment/... などの操作が必要です。

シリーズ一覧

第1部:クラスタ構築

第2部:ワークロード管理

第3部:ネットワーク

第4部:ストレージ

第5部:監視・運用

第6部:トラブルシュート

広告
kubernetes
スポンサーリンク