コンテンツへスキップ

Apple Silicon Mac で Kubernetes The Hard Way 2026年版 - Tier 1 深堀り編 + 公式の隠れたバグ G25

Apple Silicon Mac で Kubernetes The Hard Way 2026年版 - Tier 1 深堀り編 + 公式の隠れたバグ G25 のアイキャッチ
Contents

    はじめに

    Part 1 で環境構築、Part 2 で Hard Way Chapter 1-12 完走まで来ました。Part 3 は完走後の深堀り です。テーマは 3 つ:

    • Tier 1 B: etcd backup/restore (動いてる K8s クラスタの etcd を入れ替えても state が生き残るか)
    • Tier 1 A: CoreDNS 手動 deploy (Hard Way には CoreDNS が無いので、kubeadm が裏でやっている工程を素手で経験する)
    • G25: Hard Way 2024 の 上流バグ (cert SAN と Service CIDR の不整合)

    特に G25 は lima や Apple Silicon に関係なく、公式 Hard Way 2024 を素直になぞった全員が踏むはずのバグです。CoreDNS deploy で初めて顕在化するので、Chapter 12 完走で満足して次に進むと気付かないという、いやらしい性質を持っています。

    完走後の観察 — kubeadm が隠しているもの

    Tier 1 に入る前に少しだけクラスタを観察します。CKA の Troubleshooting 視点で「普段 kubeadm が裏でやっていることが、Hard Way だとどう見えるか」を確認するパートです。

    最初に default/kubernetes Service の正体:

    Terminal window
    kubectl describe svc kubernetes
    # Selector: <none>
    # Endpoints: 192.168.105.3:6443

    普通の Service は label selector で Pod を見つけますが、default/kubernetes だけは selector を持たない特殊 Service で、apiserver 自身が自分の IP:port を Endpoints に手動書き込みします。endpoints-controller が触らないので apiserver が独占管理する設計です (これが後で G25 の伏線になります)。

    次に CoreDNS 不在の operational signature:

    Terminal window
    kubectl get events --sort-by='.lastTimestamp' -A | tail -5
    # Warning MissingClusterDNS pod/nginx-... kubelet does not have ClusterDNS IP configured
    # and cannot create Pod using "ClusterFirst" policy.
    # Falling back to "Default" policy.

    kubelet が clusterDNS 未設定なので、Pod 作成時に毎回 warning を出して Default policy にフォールバック → Pod の resolv.conf は host (lima) の DNS を継承するという動作になっています。これを Tier 1 A で潰す予定。

    最後に RBAC の確認:

    Terminal window
    kubectl auth can-i create pods --as=system:node:node-0
    # no

    kubelet (system:node:<name>) は 任意の Pod 作成権限を持たない。これが「kubelet が compromise されても他ノードに横展開できない」セキュリティモデルで、Node Authorizer + NodeRestriction admission plugin の効果です。

    こうした「クラスタが動いた後に何が見えるか」を素手で観察するだけで、CKA の Troubleshooting で問われるレイヤがどこにあるか分かるようになります。

    Tier 1 B: etcd backup / restore で G24 を踏む

    CKA の Troubleshooting 30% に直結する重要訓練。動いてる K8s クラスタの etcd を入れ替えて state が生き残るか確認するシナリオです。

    Step 1: snapshot save

    Terminal window
    ssh root@server etcdctl snapshot save /tmp/etcd-backup.db
    # 881 kB, took 20.3ms

    snapshot は 20ms で 881KB。動いている nginx Deployment + Secret + Node 状態がぜんぶこの 1 ファイルに入っています。

    Step 2: G24 — etcdctl snapshot restore が消えていた

    Terminal window
    ssh root@server etcdctl snapshot restore /tmp/etcd-backup.db --data-dir=/var/lib/etcd-restored
    # Error: unknown flag: --data-dir

    公式手順そのまま打ってるはずなのに unknown flag。調べると etcd 3.6 で snapshot restoreetcdctl から etcdutl に分離されていました (Hard Way 2024 では etcd 3.6.0-rc.3 が指定される)。

    設計判断としては「restore は offline 操作 (etcd プロセス停止前提) なので、稼働中の cluster client etcdctl とは責務が違う」という分離。理屈は分かるけど初見では刺さります。

    Step 3: etcdutl 入手 + restore

    Terminal window
    cd /tmp
    ETCD_VERSION=v3.6.0-rc.3
    curl -LO https://github.com/etcd-io/etcd/releases/download/${ETCD_VERSION}/etcd-${ETCD_VERSION}-linux-arm64.tar.gz
    tar -xzf etcd-${ETCD_VERSION}-linux-arm64.tar.gz
    cp etcd-${ETCD_VERSION}-linux-arm64/etcdutl /usr/local/bin/
    etcdutl snapshot restore /tmp/etcd-backup.db --data-dir=/var/lib/etcd-restored

    ログを読むと面白い:

    "cluster-id": "cdf818194e3a8c32" ← 元の cluster identity 保持
    "local-member-id": "0" ← 新メンバーは ID 0 (フレッシュ)
    "added-peer-id": "8e9e05c52164694d" ← snapshot から再構成された旧 member

    cluster ID は保持しつつ、member ID は新規発行される設計。この挙動は Part 2 で Chapter 7 完了直後の etcdctl member list の出力と見比べるとよく分かります。

    Step 4: etcd 入れ替え (本番想定の手順)

    Terminal window
    systemctl stop etcd # 1. etcd 停止 (apiserver は connection refused を出すが死なない)
    mv /var/lib/etcd /var/lib/etcd.broken # 2. 旧 data-dir 退避 (失敗時の安全網)
    mv /var/lib/etcd-restored /var/lib/etcd # 3. restored を本番位置に
    chmod 700 /var/lib/etcd
    systemctl start etcd # 4. etcd 起動

    Step 2 の「旧 data-dir を消さずに退避する」のが本番の鉄則。restore に失敗したら戻せるようにするためです。

    Step 5: K8s state 検証 (jumpbox から)

    Terminal window
    kubectl get pods
    # nginx-54c98b4f84-p42pl 1/1 Running 0 33m ← AGE 完全保持
    kubectl get nodes
    # node-0 Ready <none> 47m v1.32.3
    # node-1 Ready <none> 47m v1.32.3

    etcd を入れ替えたのに AGE が連続している のがポイント。creationTimestamp が etcd に永続化されているので、snapshot から復元すれば 元の状態にそのまま戻る

    得た学びは大きく 3 つ:

    • decoupled architecture。control plane と workload plane が分離されているので、etcd 停止 ≈ apiserver 機能停止だがPod は走り続ける。kubelet と containerd は etcd を直接触らない
    • K8s state = etcd の中身。動いている Pod は etcd の中身ではないので、kubelet が container 状態を再 watch するだけで再構成される
    • CKA 試験対策として、「snapshot save → 故障シミュレーション → restore で復旧」を 5 分以内で実行できる練習は試験本番で効く (出題頻度高)

    Tier 1 A: CoreDNS を手動 deploy する

    「Hard Way には CoreDNS が無い」を埋めて完成形にする作業。kubeadm が裏でやっている DNS deploy 工程を素手で経験できます。

    Step 0: 現状確認

    Terminal window
    ssh root@server grep service-cluster-ip-range /etc/systemd/system/kube-apiserver.service
    # (空) → flag 未指定 → apiserver default の 10.0.0.0/24
    kubectl get svc -A
    # default/kubernetes 10.0.0.1, default/nginx 10.0.0.224 → Service CIDR 10.0.0.0/24 確定

    apiserver の --service-cluster-ip-range未指定 で default の 10.0.0.0/24 が採用されています。これが後で G25 を踏み抜く決定打になるので、頭の片隅に置いておいてください。

    Step 1-2: CoreDNS manifest 取得 + apply

    CoreDNS 公式 deploy.sh で manifest 生成:

    Terminal window
    mkdir -p ~/coredns && cd ~/coredns
    curl -LO https://raw.githubusercontent.com/coredns/deployment/master/kubernetes/coredns.yaml.sed
    curl -LO https://raw.githubusercontent.com/coredns/deployment/master/kubernetes/deploy.sh
    chmod +x deploy.sh
    apt-get install -y jq
    ./deploy.sh -i 10.0.0.10 -d cluster.local > coredns.yaml
    kubectl apply -f coredns.yaml

    Service IP は 10.0.0.10 (Service CIDR 10.0.0.0/24 内の慣例値)、Cluster Domain は cluster.local。apply は成功します。

    Step 3: G25 発覚 — CoreDNS が READY 0/1 で停滞

    ところが Pod が Running には行くものの、READY 0/1 で固まる。logs を見ると:

    [WARNING] plugin/kubernetes: Kubernetes API connection failure:
    Get "https://10.0.0.1:443/version":
    x509: certificate is valid for 127.0.0.1, 10.32.0.1, not 10.0.0.1
    [INFO] plugin/ready: Still waiting on: "kubernetes"

    CoreDNS は in-cluster Pod として動いているので、apiserver には default/kubernetes Service IP 10.0.0.1 経由で繋ぎにいきます。ところが apiserver の TLS 証明書は 10.32.0.1 が SAN に入っていて、10.0.0.1 は入っていない。だから x509 検証で蹴られる。

    これが本シリーズの一番の見どころ、Hard Way 2024 の上流バグ G25 です。

    G25 の正体: テンプレート 2 ファイル間の値の不整合

    原因を辿ると、Hard Way 2024 のテンプレート 2 ファイルで Service CIDR の前提値が食い違っている:

    ファイル出所
    ca.conf[kube-api-server_alt_names]IP.1 = 10.32.0.1旧 Hard Way の Service CIDR 10.32.0.0/24 の最初の IP
    units/kube-apiserver.service--service-cluster-ip-range フラグ未指定apiserver default の 10.0.0.0/24 を採用

    結果として:

    • apiserver は 10.0.0.0/24 を Service CIDR として運用する
    • default/kubernetes Service の ClusterIP は 10.0.0.1 (apiserver が起動時に Service CIDR の最初の IP を自動取得)
    • kube-api-server.crt の SAN は 10.32.0.1 だけで、10.0.0.1 を含まない
    • in-cluster Pod が Service IP 10.0.0.1 経由で apiserver に TLS 接続 → cert 検証失敗

    なぜ Chapter 12 完走まで顕在化しなかったのか、というと、Pod から apiserver に話しかけたクライアントがいなかったからです。これまでの接続クライアントは:

    • DNS 名 server.kubernetes.local (cert SAN にあり OK)
    • 127.0.0.1 (admin kubeconfig、cert SAN にあり OK)
    • lima IP 192.168.105.3 (実は cert に無いが誰も使ってないので問題化せず)

    の 3 系統で、どれも cert に含まれる名前 / IP でしか apiserver に到達していなかった。初の被害者が in-cluster Pod (CoreDNS) になったわけです。

    解決: cert を再発行 (key 維持、SAN に 10.0.0.1 追加)

    3 つの選択肢を比較した結果、cert 再発行を採用しました:

    概要評価
    A. cert 再発行 (採用)SAN に 10.0.0.1 を追加して再発行10 分で完了。本番でもよくやる正攻法
    B. apiserver の Service CIDR を 10.32.0.0/24 に変更--service-cluster-ip-range=10.32.0.0/24 を追加既存 Service (10.0.0.x) と etcd 内の default/kubernetes Service IP が衝突。リカバリ作業大
    C. CoreDNS が insecure TLS で apiserver に繋ぐ--insecureSkipTLSVerify 系の hackセキュリティホール、本番運用不可

    案 A の手順:

    Terminal window
    # 1. ca.conf に IP.2 = 10.0.0.1 を追加
    sed -i '/^IP\.1[[:space:]]*=[[:space:]]*10\.32\.0\.1/a IP.2 = 10.0.0.1' ca.conf
    # 2. CSR + cert 再発行 (key は維持)
    openssl req -new -key kube-api-server.key -sha256 \
    -config ca.conf -section kube-api-server -out kube-api-server.csr
    openssl x509 -req -days 3653 -in kube-api-server.csr \
    -copy_extensions copyall -sha256 \
    -CA ca.crt -CAkey ca.key -CAcreateserial \
    -out kube-api-server.crt
    # 3. SAN 検証
    openssl x509 -in kube-api-server.crt -text -noout | grep -E "IP Address:"
    # IP Address:127.0.0.1, IP Address:10.32.0.1, IP Address:10.0.0.1, ...
    # 4. 配布 + 反映
    scp kube-api-server.crt root@server:/var/lib/kubernetes/
    ssh root@server systemctl restart kube-apiserver
    kubectl rollout restart deployment coredns -n kube-system

    ポイントは key を維持して cert だけ再発行する こと。これは本番でもよくやる「証明書の SAN を後から拡張する」操作で、同じ手順で certificate expiration の延長もできます。openssl req -new -key <既存の key> の組み合わせを覚えておくと PKI rotation 全般で効きます。

    cert 再発行後、CoreDNS は 1/1 Running に到達し、DNS 解決テスト 4 種すべて成功:

    Terminal window
    kubectl exec -it $POD -- getent hosts nginx
    # 10.0.0.224 nginx.default.svc.cluster.local (短名→FQDN補完)
    kubectl exec -it $POD -- getent hosts kubernetes
    # 10.0.0.1 kubernetes.default.svc.cluster.local (apiserver の Service)
    kubectl exec -it $POD -- getent hosts kube-dns.kube-system.svc.cluster.local
    # 10.0.0.10 kube-dns.kube-system.svc.cluster.local (cross-namespace)
    kubectl exec -it $POD -- getent hosts google.com
    # 2404:6800:400b:c005::71 google.com (外部 forward)

    G25 から得た学び

    • CKA Troubleshooting (30%) に直結: ログで x509: certificate is valid for X, Y, not Z を見たら 「server cert の SAN を確認」 が定石
    • K8s の default/kubernetes Service IP は apiserver が Service CIDR の最初の IP を自動取得 する: 10.0.0.0/24 なら 10.0.0.110.32.0.0/24 なら 10.32.0.1
    • 本来 Hard Way の ca.confunits/kube-apiserver.service同期して書かれるべき だが、2024 年版は不整合のまま push されている (公式に PR を投げる候補)
    • このバグは lima 固有ではない。Apple Silicon でも GCP でも、Hard Way 2024 を素直になぞった全員が踏むはず

    CIDR の階層を整理する

    Tier 1 A までやると、Hard Way で出てくる CIDR が 6 つあると気付きます。整理すると:

    CIDR用途観察値
    192.168.105.0/24lima shared network (物理層)server 192.168.105.3, node-X 192.168.105.4-5
    192.168.5.0/24lima usernet (Mac ホスト経由)host.lima.internal 192.168.5.2
    10.0.0.0/24K8s Service CIDR (apiserver default)kubernetes 10.0.0.1, kube-dns 10.0.0.10, nginx 10.0.0.224
    10.200.0.0/24node-0 の Pod CIDRnginx Pod 10.200.0.2
    10.200.1.0/24node-1 の Pod CIDR(動作中の Pod なし)
    10.200.0.0/16Pod CIDR の親 (各 node /24 を切り出す範囲)-

    K8s が「Service IP は仮想 (kube-proxy が DNAT)、Pod IP は実体 (CNI/route で疎通)、物理 IP は別レイヤ」という 3 層構造で動いていることが、自分の手で構築すると体に染み込みます。Service CIDR と Pod CIDR を混同していると CKA / 実務でハマるので、ここで一度整理しておくのが効きます。

    まとめ

    Part 3 で扱った内容と所要時間:

    Tier 1 タスク時間
    完走後の観察 (apiserver Service / RBAC / etcd raw dump)約 20 分
    Tier 1 B: etcd backup/restore (G24 込み)約 25 分
    Tier 1 A: CoreDNS deploy (G25 検出・修正込み)約 45 分
    合計約 90 分

    Part 1 の環境構築 4 時間 + Part 2 の Hard Way 完走 4 時間 + Part 3 の Tier 1 1.5 時間で、延べ約 9.5 時間 で「環境構築 → 完走 → 内部理解」までを 1 周。

    新規に発見したハマりポイントは通算 6 件 (G20-G25)、うち G25 だけは公式 Hard Way 2024 の上流バグで、lima 関係なく全員が踏む性質を持っています。

    学びの圧縮:

    • PKI rotation は実務スキル。「key を維持して cert だけ再発行 (SAN 拡張 / 期限延長)」は Hard Way の文脈を超えて使える
    • etcd backup/restore は CKA の核。動いてる K8s の etcd を入れ替えても AGE が連続する こと自体が、K8s の decoupled architecture の証明になる
    • CoreDNS deploy が Hard Way の隠れた最終 boss。kubeadm が裏でやっていた DNS のデプロイ工程を素手で経験できる
    • 公式テンプレートでも疑うca.confunits/*.service は同期しているはず、という前提が崩れる例が現実に存在する

    これで Apple Silicon Mac × Hard Way × 2024 版のシリーズは完結です。読みながら同じ場所で詰まっていた誰かの 9.5 時間 を返せたら何より。

    次は TCP/IP マスタリング 入門編 を読みつつ、ip netns + iptables で同じレイヤを 1 VM 内で再現する実験編に進む予定 (これは別シリーズで)。


    関連リンク

    X Facebook B! Hatena