Skip to content
Published on

Kubespray徹底解説 — Ansibleで構築するプロダクション級オンプレKubernetes

Authors

はじめに

マネージドKubernetes(EKS、GKE、AKS)が事実上の標準となった時代でも、オンプレミスのベアメタルにKubernetesを直接構築しなければならない組織は依然として数多く存在します。むしろ近年は、GPUファーム構築ブームとクラウド回帰(cloud repatriation)の流れによって、オンプレKubernetesの需要が再び増えているというのが現場の実感です。

問題は、「Kubernetesを自前でインストールする」という作業が想像よりはるかに広い範囲に及ぶことです。kubeadmが解決するのはコントロールプレーンのブートストラップだけで、その前後にあるOS準備、コンテナランタイムのインストール、etcdトポロジー、CNIの展開、ロードバランサ、証明書の更新、アップグレードのオーケストレーションは、すべて運用者の責任として残ります。この隙間を埋めるツールがKubesprayです。

本記事では、Kubesprayが正確には何であり内部でどう動くのか、プロダクションクラスタを構築する際にインベントリと変数をどう設計すべきか、そして構築後の運用(ノード増設、アップグレード、バックアップ、ハードニング)をどのようにプレイブックへ落とし込むのかを、コード中心に深く掘り下げます。最後にCluster APIとの関係を整理し、「いつKubesprayを使い、いつ移行すべきか」の判断基準も提示します。基準バージョンは2026年上半期時点のKubespray v2.28系、Kubernetes v1.32系です。

オンプレKubernetesがいまだに必要とされる理由

クラウドファースト戦略が一般化した現在でも、オンプレ/ベアメタルKubernetesがなくならない理由は明確です。

第一に、データ主権と規制です。韓国金融圏における電子金融監督規定上の網分離要件、公共機関のセキュリティガイドライン、欧州のGDPRデータ所在要件などは、ワークロードがどの物理的な場所で実行されるかを直接規律します。勘定系と連動するワークロードを内部網で運用しなければならない銀行や、外部網と内部網が物理的に分離された公共環境では、パブリッククラウド自体が選択肢から外れることが少なくありません。

第二に、GPUファームです。LLMの学習・推論インフラを自前で構築する組織が増え、数十から数百台のGPUサーバーをKubernetesで束ねる事例が急増しました。高価なGPUをクラウドで時間単位で借りるより、自社保有して稼働率を高めるほうが総所有コスト(TCO)の観点で有利になる損益分岐点が確かに存在します。

第三に、コストです。トラフィックとリソース使用量が予測可能で一定規模を超えるワークロードでは、減価償却を考慮しても自社データセンターやコロケーションのほうが安くなる領域があります。特にエグレストラフィック費用とブロックストレージ費用は、オンプレ回帰を決断させる定番の要因です。

第四に、レイテンシと特殊ハードウェアです。工場ラインのエッジコンピューティング、取引所とのコロケーションが必要なトレーディングシステム、SR-IOVやDPDKのような特殊なネットワーク機構を使う通信事業者のNFV環境は、物理インフラへの直接的な制御が前提条件になります。

こうした環境では「どうやってKubernetesをインストールし維持していくか」がすぐ次の問いになります。まずその答えの候補を比較してみましょう。

構築ツールの勢力図 — 何でインストールするか

オンプレKubernetesの構築ツールは大きく5つの系統に分かれます。

ツールアプローチ対象OSHAコントロールプレーンエアギャップ対応適した環境
kubeadm手動CLIで段階的にブートストラップ汎用Linux自前で構成自前で構成学習、小規模、フルカスタム
KubesprayAnsibleプレイブックでkubeadmをオーケストレーションUbuntu、RHEL、Rocky、Debianなど汎用内蔵(複数CP+LBオプション)公式サポート(オフラインミラー)汎用ベアメタル/VM、異種OS、細かなカスタム
kOpsクラスタライフサイクルCLIクラウド中心(AWS、GCE)内蔵限定的クラウドのセルフマネージド、オンプレには不向き
RKE2 / k3s独自ディストリビューション(単一バイナリ志向)汎用Linux内蔵良好エッジ、セキュリティ重視(FIPS)、Rancherエコシステム
Talos LinuxKubernetes専用の不変OSTalos専用OS内蔵良好OSまで統制可能な新規構築、セキュリティ最大化

選定基準を一行ずつ要約します。

  • kubeadm手動構成はKubernetesの内部を理解する最良の教材ですが、数十台規模の反復可能な構築には自動化レイヤーが必要です。
  • kOpsはAWSのセルフマネージドクラスタに強い一方、ベアメタルのストーリーは事実上ありません。
  • RKE2とk3sはインストールが最も簡単でセキュリティのデフォルトも良好ですが、アップストリームのkubeadm経路ではない独自ディストリビューションである点、Rancherエコシステムへの依存が生じる点を考慮すべきです。
  • TalosはSSHすら存在しない不変OSという最も急進的で魅力的なアプローチですが、既存のOS標準(セキュリティエージェント、資産管理など)に従う必要がある組織では導入障壁が高くなります。
  • Kubesprayは「組織標準のLinuxがすでにあり、その上にアップストリームKubernetesを細かく制御しながら載せたい」という最も一般的なエンタープライズ要件にぴったり合います。既にAnsibleを使っている組織なら学習曲線も緩やかです。

本記事の主役は最後の選択肢、Kubesprayです。

Kubesprayの正体 — kubeadmを包むAnsibleプレイブック

Kubesprayはkubernetes-sigs配下のCNCFプロジェクトで、本質は「プロダクション級Kubernetesクラスタを構成するAnsibleプレイブックとロールの集合体」です。魔法のような独自エンジンがあるわけではなく、実績ある構成要素をAnsibleでオーケストレーションします。

  • コントロールプレーンのブートストラップは内部的にkubeadmを呼び出します。つまりKubesprayで作ったクラスタはkubeadmクラスタであり、kubeadmの証明書体系とアップグレード経路をそのまま踏襲します。
  • OS準備(スワップ無効化、カーネルモジュール、sysctl)、containerdのインストール、etcdクラスタ構成、CNI(Calico、Cilium、flannelなど)の展開、アドオン(CoreDNS、MetalLB、ingress-nginxなど)のインストールまで、全区間をロールでカバーします。
  • サポート範囲が広いのも特徴です。多様なLinuxディストリビューション、複数のCNI、エアギャップ環境、GPUノード、多様なトポロジー(etcd分離/同居)を変数で選択できます。

全体の流れをASCIIで描くと次のようになります。

+--------------------------------------------------------------------+
| Ansibleコントロールノード(運用者PCまたは踏み台)                     |
|                                                                    |
|  inventory/prod/                                                   |
|   ├─ hosts.yaml            ← ノード一覧とグループ(トポロジー)定義   |
|   └─ group_vars/                                                   |
|       ├─ all/all.yml       ← 全域変数(LB、プロキシ、レジストリ)     |
|       └─ k8s_cluster/                                              |
|           ├─ k8s-cluster.yml  ← バージョン、CIDR、CNI、プロキシ     |
|           └─ addons.yml       ← ingress、metallb、cert-manager     |
|                                                                    |
|  ansible-playbook cluster.yml                                      |
|        │                                                           |
|        ▼                                                           |
|  [ロール実行順序]                                                   |
|   1. kubernetes/preinstall  → OS検証、sysctl、モジュール、スワップ  |
|   2. container-engine       → containerd / crio のインストール     |
|   3. download               → バイナリ・イメージ取得(またはミラー)  |
|   4. etcd                   → etcdクラスタ構成・証明書              |
|   5. kubernetes/control-plane → kubeadm init / join (CPノード)     |
|   6. kubernetes/node        → kubelet構成、kubeadm join (ワーカー)  |
|   7. network_plugin         → Calico / Cilium マニフェスト適用      |
|   8. kubernetes-apps        → CoreDNS、アドオン、MetalLBなど        |
+--------------------------------------------------------------------+
        │ SSH (become: root)
+-------------------+  +-------------------+  +-------------------+
|  cp1 (CP+etcd)    |  |  cp2 (CP+etcd)    |  |  cp3 (CP+etcd)    |
+-------------------+  +-------------------+  +-------------------+
+-------------------+  +-------------------+  +-------------------+
|  worker1          |  |  worker2          |  |  workerN ...      |
+-------------------+  +-------------------+  +-------------------+

重要な洞察は2つです。第一に、Kubesprayは宣言的コントローラではなく手続き的な実行ツールです。プレイブックを実行した瞬間にだけ状態を収束させ、実行しなければ何もしません。第二に、インベントリとgroup_varsがそのままクラスタの形状定義なので、このディレクトリをGitで管理することが運用の出発点になります。

事前準備 — ノード、ネットワーク、アクセス

ノード要件

プロダクション基準では次を推奨します。

  • コントロールプレーン: 3台(クォーラム維持のため奇数)、最低2 vCPU / 4GB RAM、推奨4 vCPU / 8GB以上。etcdを同居させる場合、ディスクは必ずローカルSSD/NVMeにします。etcdはfsyncレイテンシに極めて敏感で、遅いディスクはクラスタ全体の不安定化に直結します。
  • ワーカー: ワークロードに応じて算定し、kubeletとシステムデーモンのための予約(systemReserved、kubeReserved)を考慮します。
  • OS: Ubuntu 22.04/24.04 LTS、RHEL 9、Rocky 9など、Kubesprayサポートマトリクスにあるディストリビューション。全ノードのOSとカーネルバージョンを統一すると、トラブルシューティングのコストが大幅に下がります。
  • 共通条件: スワップ無効化(Kubesprayが処理しますが、fstabの恒久設定を確認)、ノードごとに一意なhostname・MAC・product_uuid、時刻同期(chrony)、そしてbr_netfilterとoverlayカーネルモジュール。

コンテナランタイムはcontainerdがデフォルトで、特別な理由がなければそのままにします。CRI-Oも選択できますが、サポートの広さはcontainerdが最良です。

ネットワーク計画 — CIDR設計

構築後の変更が事実上不可能な値なので、最も慎重になるべき部分です。3つのレンジが互いに、そして社内ネットワークと絶対に重複してはいけません。

ノードネットワーク : 10.10.0.0/24    (物理サーバーIP — 社内IPAMから割当)
Pod CIDR          : 10.233.64.0/18  (kube_pods_subnet — デフォルト)
Service CIDR      : 10.233.0.0/18   (kube_service_addresses — デフォルト)

ノードあたりPodレンジ: /24 (kube_network_node_prefix)
→ ノードあたり最大約110 Pod、/18基準で最大64ノード

規模算定の式は単純です。Pod CIDRのサイズからノードあたりのprefixを引けば、収容可能なノード数が出ます。例えば/18のPodレンジでノードあたり/24を与えると2の6乗、つまり64ノードが上限です。300ノード規模を計画するなら、Podレンジを/16に広げるか、ノードあたりprefixを/25に縮める決定を構築前に行う必要があります。また社内網と重複すると、そのレンジの社内システムとの通信が断絶するため、ネットワークチームとともにIPAMドキュメントへこれらのレンジを正式登録しておくことを勧めます。

コントロールプレーンHA — APIサーバーの前のVIP

コントロールプレーンが3台あっても、クライアント(kubelet、kubectl、CI)が見るエンドポイントが1台のノードIPなら、そのノードが単一障害点です。解決策は3つあります。

  1. kube-vip: コントロールプレーンノード上にスタティックPodとして常駐し、ARP(またはBGP)でVIPを広告します。外部機器が不要で、ベアメタルでは最も手軽です。Kubesprayはkube_vip_enabled変数で直接サポートします。
  2. HAProxy + keepalived: 専用LBノード2台でHAProxyが6443ポートをCP3台のバックエンドへ分散し、keepalivedのVRRPでVIPをフェイルオーバーします。伝統的で実績のある方式で、LB層をネットワークチームが管理する組織に適しています。
  3. 外部ハードウェアLB: すでにL4機器(F5など)があるなら、それを活用します。

Kubesprayにはもう一つ内蔵の仕組みがあります。loadbalancer_apiserver_localhostオプション(デフォルト有効)は各ノードにnginx-proxyスタティックPodを立て、localhostからCP3台へ分散します。つまりクラスタ内部コンポーネントのHAは外部LBなしでもある程度確保されますが、クラスタ外部からアクセスするkubectlとCIのためには、依然としてVIPが必要です。

SSHとsudo

Ansibleが全ノードにSSHで接続しroot権限で作業するため、次を準備します。

# コントロールノードで鍵を生成し全ノードへ配布
ssh-keygen -t ed25519 -f ~/.ssh/kubespray_ed25519
for h in 10.10.0.11 10.10.0.12 10.10.0.13 10.10.0.21 10.10.0.22; do
  ssh-copy-id -i ~/.ssh/kubespray_ed25519.pub deploy@"$h"
done

# deployアカウントへパスワードなしsudoを付与(各ノード)
echo 'deploy ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/deploy

エアギャップ(閉域網)環境の準備概要

網分離環境では、本来インターネットから取得する3種類の成果物を内部ミラーへ移す必要があります。

  1. コンテナイメージ → 内部レジストリ(Harborなど)
  2. バイナリファイル(kubeadm、kubelet、etcd、CNIプラグイン、crictlなど) → 内部HTTPサーバー
  3. OSパッケージ(containerdの依存など) → 内部yum/aptリポジトリ

Kubesprayはcontrib/offlineディレクトリに、必要なイメージ・ファイル一覧を抽出するスクリプトを同梱しています。インターネット接続可能な準備区域で一覧を生成してミラーを充填し、閉域網のインベントリではダウンロードURL変数を内部ミラーへ差し替える方式で進めます。具体的な変数は後述のカスタマイズ節で扱います。

ハンズオン — クラスタ構築の全フロー

ステップ1: Kubesprayの準備とインベントリ作成

# 必ずリリースタグをチェックアウト — masterブランチでのプロダクション構築は禁止
git clone --branch v2.28.0 https://github.com/kubernetes-sigs/kubespray.git
cd kubespray

# Ansibleバージョン衝突を避けるため専用の仮想環境を使用
python3 -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -r requirements.txt

# サンプルインベントリをコピーしてプロダクション用を作成
cp -rfp inventory/sample inventory/prod

リリースタグの固定は単なる推奨ではありません。Kubesprayのリリースごとに、サポートするKubernetesマイナーバージョンとコンポーネントバージョンのマトリクスが固定されており、このマトリクスを外れた組み合わせはテストされていない領域です。

ステップ2: hosts.yaml — トポロジー定義

コントロールプレーン3台(etcd同居)とワーカー3台の標準構成です。

# inventory/prod/hosts.yaml
all:
  hosts:
    cp1:
      ansible_host: 10.10.0.11
      ip: 10.10.0.11          # kubelet/etcdがバインドする内部IP
      access_ip: 10.10.0.11   # 他ノードがこのノードへ到達するIP
    cp2:
      ansible_host: 10.10.0.12
      ip: 10.10.0.12
      access_ip: 10.10.0.12
    cp3:
      ansible_host: 10.10.0.13
      ip: 10.10.0.13
      access_ip: 10.10.0.13
    worker1:
      ansible_host: 10.10.0.21
      ip: 10.10.0.21
    worker2:
      ansible_host: 10.10.0.22
      ip: 10.10.0.22
    worker3:
      ansible_host: 10.10.0.23
      ip: 10.10.0.23
  children:
    kube_control_plane:
      hosts:
        cp1:
        cp2:
        cp3:
    kube_node:
      hosts:
        worker1:
        worker2:
        worker3:
    etcd:
      hosts:
        cp1:
        cp2:
        cp3:
    k8s_cluster:
      children:
        kube_control_plane:
        kube_node:
    calico_rr:
      hosts: {}

トポロジー設計のポイントは次のとおりです。

  • etcdグループをコントロールプレーンから分離し、専用ノード3台に置くこともできます(スタック型 vs 外部etcd)。大規模クラスタ(数百ノード)やAPIサーバー負荷が大きい環境では外部etcdが安定しますが、管理ノードが6台に増えます。50ノード以下ならスタック型で十分な場合がほとんどです。
  • ipとaccess_ipを明示する習慣をつけてください。NICが複数あるサーバーでAnsibleが誤ったインターフェースIPを自動検出し、etcdが想定外のレンジにバインドされる事故は頻発します。
  • グループ名(kube_control_plane、kube_node、etcd、k8s_cluster)はKubesprayのロールが参照する予約語なので変更してはいけません。

ステップ3: group_varsの解剖 — k8s-cluster.yml

クラスタのアイデンティティを決めるファイルです。核心となる変数だけ抜き出します。

# inventory/prod/group_vars/k8s_cluster/k8s-cluster.yml

# Kubernetesバージョン — 当該Kubesprayリリースのサポート範囲内のみ
kube_version: "1.32.5"

# ネットワークプラグイン: calico、cilium、flannel、kube-ovnなど
kube_network_plugin: calico

# CIDR — 構築後の変更不可、社内レンジとの重複禁止
kube_service_addresses: 10.233.0.0/18
kube_pods_subnet: 10.233.64.0/18
kube_network_node_prefix: 24

# kube-proxyモード: ipvs推奨(サービス数千件規模でiptablesより有利)
kube_proxy_mode: ipvs

# コンテナランタイム
container_manager: containerd

# DNS
dns_mode: coredns
enable_nodelocaldns: true        # ノードローカルDNSキャッシュ — 大規模では必須級

# クラスタ名(内部ドメイン)
cluster_name: cluster.local

# 構築完了後にコントロールノードへadmin kubeconfigをコピー
kubeconfig_localhost: true

# 証明書の自動更新(systemd timerが毎月更新を試行)
auto_renew_certificates: true

変数ごとの判断基準を押さえておきましょう。

  • kube_network_plugin: デフォルトのCalicoはBGPベースのルーティングとネットワークポリシーで実績ある選択です。eBPFデータプレーン、Hubbleによる可観測性、L7ポリシーが必要ならCiliumを選びます。重要なのは、この選択が事実上恒久的だという点です。稼働中のクラスタでCNIを入れ替えることはKubesprayがサポートするシナリオではありません。
  • kube_proxy_mode: ipvsはサービス数が多い場合にiptablesモードの線形探索コストを回避できます。なお、Ciliumをkube-proxy replacementモードで使えば、kube-proxy自体を排除する構成も可能です。
  • enable_nodelocaldns: PodのDNS問い合わせをノードローカルキャッシュが受け、CoreDNSの負荷とconntrackの競合を減らします。DNSタイムアウトに苦しんだ経験のある運用者なら、デフォルト有効を勧めます。

APIサーバーエンドポイント(VIP)はallグループの変数に定義します。

# inventory/prod/group_vars/all/all.yml

# 外部LB(HAProxy+keepalivedまたはハードウェアLB)を使う場合
apiserver_loadbalancer_domain_name: "k8s-api.prod.internal"
loadbalancer_apiserver:
  address: 10.10.0.100   # VIP
  port: 6443

# 各ノードのnginx-proxyローカルLB(デフォルト有効 — 内部コンポーネントのHA)
loadbalancer_apiserver_localhost: true

kube-vipを使う場合は、専用LBノードなしで次のように設定します。

# kube-vipによるコントロールプレーンVIP構成(ARPモード)
kube_vip_enabled: true
kube_vip_controlplane_enabled: true
kube_vip_arp_enabled: true
kube_vip_address: 10.10.0.100
loadbalancer_apiserver:
  address: 10.10.0.100
  port: 6443

ステップ4: group_varsの解剖 — addons.yml

クラスタの上に載せる基本アドオンを選択します。

# inventory/prod/group_vars/k8s_cluster/addons.yml

helm_enabled: true
metrics_server_enabled: true

# Ingressコントローラ
ingress_nginx_enabled: true
ingress_nginx_host_network: false

# ベアメタルにおけるLoadBalancerサービスの実装 — MetalLB
metallb_enabled: true
metallb_speaker_enabled: true
metallb_config:
  address_pools:
    primary:
      ip_range:
        - 10.10.0.150-10.10.0.180
      auto_assign: true
  layer2:
    - primary

# 証明書の自動化
cert_manager_enabled: true

# シンプルなローカルPVで足りる場合
local_path_provisioner_enabled: true

ベアメタルにはクラウドのようにLoadBalancerタイプのServiceを実現してくれる主体がないため、MetalLBがその役割を担います。L2モードは設定が単純な代わりにトラフィックが単一ノードへ収束する限界があり、BGPモードはToRスイッチとのピアリングが必要ですが真の分散が可能です。ネットワークチームとの調整ができるなら、BGPモードは検討の価値があります。なお、アドオンについては「初期ブートストラップだけKubesprayで入れ、その後のライフサイクルはHelm/GitOpsで直接管理する」戦略も広く使われています。Kubesprayのアドオン変数は便利ですが、チャートバージョン選択の自由度が低いためです。

ステップ5: 実行 — cluster.yml

# 事前の疎通確認
ansible -i inventory/prod/hosts.yaml all -m ping \
  --private-key ~/.ssh/kubespray_ed25519 -u deploy -b

# 本実行 — 6ノードで約20〜40分
ansible-playbook -i inventory/prod/hosts.yaml \
  --private-key ~/.ssh/kubespray_ed25519 -u deploy \
  --become --become-user=root \
  cluster.yml

実行中に画面を流れるステージは、前述のアーキテクチャ図のロール順序と一致します。注視すべき区間は3つです。downloadロールで失敗するならネットワーク/プロキシ/ミラーの問題、etcdロールで止まるならノード間の2379/2380ポート疎通かip変数の設定問題、control-planeロールのkubeadm init後のヘルスチェックで失敗するならVIPか証明書SANの構成問題である可能性が高いです。

ステップ6: 検証

# kubeconfig_localhost: true ならインベントリ配下にadmin.confがコピーされる
export KUBECONFIG=$PWD/inventory/prod/artifacts/admin.conf

kubectl get nodes -o wide
# NAME      STATUS   ROLES           VERSION   INTERNAL-IP
# cp1       Ready    control-plane   v1.32.5   10.10.0.11
# cp2       Ready    control-plane   v1.32.5   10.10.0.12
# cp3      Ready    control-plane   v1.32.5   10.10.0.13
# worker1   Ready    <none>          v1.32.5   10.10.0.21
# ...

# コントロールプレーンPodとCNIの状態
kubectl get pods -n kube-system

# etcdメンバーとヘルス(cp1で実行)
sudo ETCDCTL_API=3 etcdctl \
  --endpoints=https://10.10.0.11:2379 \
  --cacert=/etc/ssl/etcd/ssl/ca.pem \
  --cert=/etc/ssl/etcd/ssl/node-cp1.pem \
  --key=/etc/ssl/etcd/ssl/node-cp1-key.pem \
  endpoint health --cluster

# スモークテスト: デプロイ → 公開 → DNS → 削除
kubectl create deployment nginx --image=nginx --replicas=3
kubectl expose deployment nginx --port=80
kubectl run dns-test --rm -it --image=busybox:1.36 --restart=Never \
  -- nslookup nginx.default.svc.cluster.local
kubectl delete deployment nginx svc/nginx

これに加えて、VIPフェイルオーバーテスト(CPノード1台を強制停止しkubectlの動作を確認)とワーカー1台のドレインテストまで通過して初めて、プロダクションの受け入れ基準を満たしたと考えるのが安全です。

Day-2運用のプレイブック

Kubesprayの真の価値は、Day-2運用がすべてプレイブックとして定型化されている点にあります。

ノード追加 — scale.yml

# 1) hosts.yamlにworker4を追加してから
# 2) 新規ノードだけを対象にscaleを実行
ansible-playbook -i inventory/prod/hosts.yaml \
  --become --limit=worker4 \
  scale.yml

scale.ymlはcluster.ymlのうち既存ノードを再構成するステップを省略し、新規ノードの準備とkubeadm joinだけを実行するため、高速かつ安全です。注意点は、limitオプションを使ってもAnsibleがetcdグループなど他ホストのファクト(facts)を必要とすることです。ファクト収集に失敗するとプレイブックが壊れるため、まず全ノードのファクトを更新する習慣を勧めます。

# limit実行の前に全体のファクトを更新
ansible-playbook -i inventory/prod/hosts.yaml --become playbooks/facts.yml

コントロールプレーンノードの追加はscale.ymlではなくcluster.ymlを使う必要があり、追加後は証明書SANとLBバックエンド一覧の更新を忘れてはいけません。

ノード削除 — remove-node.yml

# ドレイン → クラスタから削除 → ノードのクリーンアップまで一括
ansible-playbook -i inventory/prod/hosts.yaml \
  --become \
  -e node=worker3 \
  remove-node.yml

remove-node.ymlは対象ノードをcordon/drainし、kubectl delete nodeとノード側のkubelet/ランタイムのクリーンアップを実行します。ノードがすでに死んでいて接続不能な場合は、reset_nodes=false変数を併用してノード側クリーンアップを省略し、クラスタのメタデータだけを削除できます。削除が終わったらhosts.yamlからも該当ノードを消し、インベントリと実態の一致を保ってください。この同期を怠ることがインベントリドリフトの始まりです。

アップグレード — upgrade-cluster.yml

最も慎重さが求められる作業です。まず原則を整理します。

  1. マイナーバージョンのスキップ禁止。1.30から1.32へ直接は行けません。kubeadmとKubernetesのバージョンスキューポリシーに従い、1.30 → 1.31 → 1.32と段階を踏み、各段階に対応するKubesprayのリリースタグへ乗り換える必要があります。
  2. Kubesprayバージョンとクラスタバージョンの対応を記録・維持してください。インベントリのリポジトリに「このクラスタはv2.27.1タグで1.31.4を運用中」という事実が残っているべきです。
  3. アップグレード前のetcdバックアップは絶対条件です。
# 例: 1.31.x → 1.32.x
cd kubespray && git checkout v2.28.0
pip install -r requirements.txt   # 要求されるAnsibleバージョンも変わる

ansible-playbook -i inventory/prod/hosts.yaml \
  --become \
  -e kube_version=1.32.5 \
  upgrade-cluster.yml

upgrade-cluster.ymlの動作こそが無停止の鍵です。

  • コントロールプレーンとetcdを先に、1台ずつ順次アップグレードします。
  • ワーカーはドレイン → kubelet/ランタイム更新 → uncordonの順で進み、同時に処理するノード数はserial変数(デフォルト20%)で制御します。保守的に進めるならserial=1を指定します。
  • ドレイン動作はdrain_grace_period、drain_timeout、drain_retries変数で調整します。PodDisruptionBudgetが厳しすぎるとドレインが無限に待つため、アップグレード前のPDB点検は必須です。
# 1台ずつ、ドレインのタイムアウトを十分に
ansible-playbook -i inventory/prod/hosts.yaml --become \
  -e kube_version=1.32.5 \
  -e serial=1 \
  -e drain_timeout=600s \
  -e drain_grace_period=120 \
  upgrade-cluster.yml

無停止のためにはアプリケーション側の要件も併せて整える必要があります。レプリカ2以上、適切なPDB、preStopフックとgraceful shutdown、そして単一レプリカStatefulSetのようなドレインブロッカーの事前洗い出しです。

reset.yml — 最終手段

# クラスタをノードから完全に撤去 — ディスク上のetcdデータも削除される
ansible-playbook -i inventory/prod/hosts.yaml --become reset.yml

reset.ymlはKubernetes、etcd、CNI設定、コンテナデータをノードから除去します。本番クラスタで「一部のノードだけ初期化しよう」としてlimitなしで実行する事故は実際に起きています。実行前の確認プロンプトはありますが、CIで自動承認にして回す構成は絶対に禁物です。

etcdのバックアップとリストア

Kubesprayはバックアップを代行しないため、自前で体制を整える必要があります。

# バックアップ(etcdノードで — cron/systemd timerで定期実行)
sudo ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-snap-20260613.db \
  --endpoints=https://10.10.0.11:2379 \
  --cacert=/etc/ssl/etcd/ssl/ca.pem \
  --cert=/etc/ssl/etcd/ssl/node-cp1.pem \
  --key=/etc/ssl/etcd/ssl/node-cp1-key.pem

# 整合性の確認
ETCDCTL_API=3 etcdctl --write-out=table snapshot status /backup/etcd-snap-20260613.db

スナップショットファイルは必ずクラスタ外部のストレージへ搬出し、四半期ごとにリストア演習(スナップショット → etcdctl snapshot restore → 新しいデータディレクトリでetcd起動)を実施して、バックアップが本当に復旧可能であることを検証すべきです。バックアップはあるのにリストア手順を一度も練習していない組織は、意外なほど多いものです。

カスタマイズ — プロダクションで必ず出会うもの

証明書管理

kubeadmクラスタのコントロールプレーン証明書はデフォルトで有効期間1年です。Kubesprayでは次の2つの変数で管理します。

# 月1回systemd timer(k8s-certs-renew.timer)が期限間近の証明書を更新
auto_renew_certificates: true

# 必要時にプレイブック実行で強制再発行
# -e force_certificate_regeneration=true

auto_renew_certificatesを有効にしておけば、更新漏れによる「1年後にクラスタ全体が認証不能」という事故を予防できます。ただし、CA自体の有効期間(デフォルト10年)と外部LBに設置した証明書は別管理の対象です。失効監視(x509 exporterなど)を併設することを勧めます。

プライベートレジストリとエアギャップの詳細設定

閉域網構築の核心は、すべてのダウンロード経路を内部ミラーへ差し替える変数セットです。

# inventory/prod/group_vars/all/offline.yml
registry_host: "harbor.internal:443/k8s-mirror"
files_repo: "https://mirror.internal/kubespray-files"

# コンテナイメージのリポジトリをすべて内部レジストリへ
kube_image_repo: "{{ registry_host }}"
gcr_image_repo: "{{ registry_host }}"
github_image_repo: "{{ registry_host }}"
docker_image_repo: "{{ registry_host }}"
quay_image_repo: "{{ registry_host }}"

# バイナリのダウンロードURLを内部ファイルサーバーへ
kubeadm_download_url: "{{ files_repo }}/kubeadm/{{ kube_version }}/kubeadm"
kubelet_download_url: "{{ files_repo }}/kubelet/{{ kube_version }}/kubelet"
kubectl_download_url: "{{ files_repo }}/kubectl/{{ kube_version }}/kubectl"
etcd_download_url: "{{ files_repo }}/etcd/etcd-{{ etcd_version }}-linux-{{ host_architecture }}.tar.gz"
cni_download_url: "{{ files_repo }}/cni/cni-plugins-linux-{{ host_architecture }}-{{ cni_version }}.tgz"
crictl_download_url: "{{ files_repo }}/crictl/crictl-{{ crictl_version }}-linux-{{ host_architecture }}.tar.gz"
runc_download_url: "{{ files_repo }}/runc/{{ runc_version }}/runc.{{ host_architecture }}"
containerd_download_url: "{{ files_repo }}/containerd/containerd-{{ containerd_version }}-linux-{{ host_architecture }}.tar.gz"

# プライベートCAを使うレジストリならcontainerdへ信頼を登録
containerd_registries_mirrors:
  - prefix: "harbor.internal"
    mirrors:
      - host: "https://harbor.internal"
        capabilities: ["pull", "resolve"]
        skip_verify: false

準備区域ではcontrib/offlineのgenerate_list.shで当該リリースが要求するファイル・イメージの全一覧を抽出し、manage-offline-container-images.shでイメージを一括取得して内部レジストリへプッシュします。運用のコツを一つ加えると、ミラー充填の作業自体もCIパイプライン化しておきましょう。アップグレードのたびに手作業で一覧を更新して漏れが出ることが、閉域網アップグレード失敗の最多原因です。

sysctlとカーネルチューニングの注入

ノードOSのチューニングを別のAnsibleロールで管理してもよいのですが、Kubespray変数で一緒に注入すれば構成が一箇所に集まります。

# inventory/prod/group_vars/k8s_cluster/k8s-cluster.yml
additional_sysctl:
  - name: net.core.somaxconn
    value: 65535
  - name: net.ipv4.tcp_max_syn_backlog
    value: 65535
  - name: fs.inotify.max_user_instances
    value: 8192
  - name: fs.inotify.max_user_watches
    value: 1048576

# kubeletにノードリソースを予約させる
kube_reserved: true
kube_memory_reserved: 512Mi
kube_cpu_reserved: 200m
system_reserved: true
system_memory_reserved: 1Gi
system_cpu_reserved: 500m

inotifyの上限は、ログコレクタと高密度なPod配置で必ず突き当たるボトルネックなので、早めに引き上げておくことを勧めます。

追加マニフェストとGitOps連携

Kubesprayには任意のマニフェストを注入する汎用フックがあまりありません。実務パターンは明確です。Kubesprayの責務を「ノードOSからCNIまで」に限定し、その上のすべて(モニタリング、ロギング、Ingressの詳細設定、アプリケーション)はArgo CDやFluxのようなGitOpsツールで管理することです。構築パイプラインの最終ステップでArgo CDのブートストラップマニフェストを一つだけkubectl applyするように構成すれば、以降のクラスタ内部の状態はすべてGitが真実の源泉になります。

プロダクションハードニング

CISベンチマークとkube-bench

CIS Kubernetes Benchmarkへの整合は、金融・公共のセキュリティ審査で定番の要求事項です。Kubesprayのデフォルトは CIS を完全には満たさないため、構築後にkube-benchを実行してギャップを確認し、変数で補正するサイクルを勧めます。よく補正する項目は次のとおりです。

# APIサーバーのハードニング
kube_apiserver_request_timeout: 120s
kube_apiserver_enable_admission_plugins:
  - NodeRestriction
  - AlwaysPullImages
  - EventRateLimit
kube_apiserver_admission_event_rate_limits:
  limit_1:
    type: Namespace
    qps: 50
    burst: 100
    cache_size: 2000
kube_profiling: false

# kubeletのハードニング
kubelet_protect_kernel_defaults: true
kubelet_event_record_qps: 1
kubelet_streaming_connection_idle_timeout: 5m
kubelet_make_iptables_util_chains: true

# 匿名認証の遮断はkubeadmデフォルトで処理されるが明示的に
kube_api_anonymous_auth: false

kube-benchの指摘をすべて消そうとするより、各項目が自分のワークロードへ与える影響(例: AlwaysPullImagesはレジストリ負荷の増加)を吟味し、例外の理由を文書化するアプローチが現実的です。

監査ログ(audit log)

kubernetes_audit: true
audit_log_path: /var/log/kubernetes/audit/audit.log
audit_log_maxage: 30        # 保存日数
audit_log_maxbackups: 10
audit_log_maxsize: 100      # MB
# デフォルトポリシーの代わりにカスタムポリシーを使うなら
# audit_policy_custom_rules にルールを定義

監査ログはセキュリティインシデント調査における事実上唯一の一次証拠なので、ノードローカルに置くだけでなく、中央ログシステムへ収集するパイプラインまでがワンセットです。

Secretsの保存時暗号化(encryption at rest)

etcdに平文で保存されるSecretを暗号化します。

kube_encrypt_secret_data: true
# デフォルトのプロバイダはsecretbox — aescbcなどへ変更可能
kube_encryption_algorithm: "secretbox"
kube_encryption_resources: [secrets]

この設定は、etcdスナップショットが流出してもSecretの原文が露出しないようにする最低限の防御線です。鍵自体がコントロールプレーンノードのディスク上にあるという限界は残るため、より高い要求水準では外部KMS連携を検討します。

Pod Security Admissionのデフォルト

PodSecurityPolicy廃止後の標準はPSAです。Kubespray変数でクラスタ全域のデフォルトを指定できます。

kube_pod_security_use_default: true
kube_pod_security_default_enforce: baseline   # 新規Namespaceへ既定適用
kube_pod_security_default_audit: restricted
kube_pod_security_default_warn: restricted
kube_pod_security_exempt_namespaces:
  - kube-system

enforceを最初からrestrictedに上げると既存ワークロードが大量に拒否される恐れがあるため、audit/warnで違反を可視化してから段階的にenforceを引き上げる順序が安全です。

CI/CD統合と大規模運用

インベントリをGitへ、実行をパイプラインへ

推奨リポジトリ構造は次のとおりです。

k8s-clusters/                  ← 社内Gitリポジトリ
├─ README.md                   ← クラスタ・Kubesprayバージョン対応表
├─ clusters/
│   ├─ prod-seoul/
│   │   ├─ hosts.yaml
│   │   └─ group_vars/ ...
│   └─ stage-seoul/
│       ├─ hosts.yaml
│       └─ group_vars/ ...
└─ pipelines/
    └─ run-kubespray.yaml      ← CI定義

Kubespray本体はサブモジュールまたはパイプライン内のgit clone(タグ固定)で取得し、インベントリの変更はすべてPRレビューを通します。実行パイプラインの骨格例です。

# GitLab CIの例 — 概念的な骨格
stages: [lint, diff, apply]

lint:
  stage: lint
  script:
    - ansible-lint clusters/prod-seoul || true
    - python3 scripts/validate_inventory.py clusters/prod-seoul

apply-prod:
  stage: apply
  when: manual            # プロダクションは必ず手動承認
  script:
    - git clone --branch v2.28.0 --depth 1
        https://github.com/kubernetes-sigs/kubespray.git
    - pip install -r kubespray/requirements.txt
    - ansible-playbook -i clusters/prod-seoul/hosts.yaml
        --become kubespray/cluster.yml
  environment: prod-seoul

冪等性の活用と限界

Ansibleの冪等性のおかげで、cluster.ymlの再実行は「変更された部分だけ収束」が基本動作であり、失敗地点からの再開も同じコマンドの再実行で十分な場合が多いです。しかし限界を正確に知る必要があります。

  • 変数から項目を「削除」しても、ノード上の既存設定が「撤回」されるわけではありません。例えばアドオンをenabled falseに変えても、すでに展開済みのリソースが自動削除されないケースがあります。手続き的ツールの本質的な限界です。
  • checkモード(ドライラン)はKubesprayでは信頼性が低いです。多くのタスクが先行タスクの実際の結果に依存するためです。「diffを見て承認」というTerraform式のワークフローは期待できないため、ステージングクラスタへ先に適用することで代替します。
  • プレイブック実行中の他の変更(手動のkubectl操作など)と衝突し得るため、実行時間帯を変更凍結ウィンドウと合わせる運用規律が必要です。

大規模運用のコツ — 並列度と部分実行

# ansible.cfg
[defaults]
forks = 50                 # デフォルト5 — 数十ノードなら必ず引き上げ
strategy = linear
[ssh_connection]
pipelining = True          # SSH往復の削減 — 体感効果が大きい
ssh_args = -o ControlMaster=auto -o ControlPersist=30m

実行時間の感覚としては、6ノードの新規構築が20〜40分、50ノードの構築が1時間以上、アップグレードはドレイン時間のためノード数にほぼ線形で比例します。部分実行の道具は2つです。

# 特定ノードのみ — 必ずfactsを更新してから
ansible-playbook -i inventory/prod/hosts.yaml --become playbooks/facts.yml
ansible-playbook -i inventory/prod/hosts.yaml --become \
  --limit=worker7 cluster.yml

# 特定コンポーネントのタグのみ — 例: CoreDNS設定変更の反映
ansible-playbook -i inventory/prod/hosts.yaml --become \
  --tags=coredns cluster.yml

# ダウンロード段階をスキップして反復実行を短縮
ansible-playbook -i inventory/prod/hosts.yaml --become \
  --skip-tags=download cluster.yml

タグとlimitは強力ですが、タスク間の依存関係を飛ばすリスクがあるため、「ステージングで同じタグの組み合わせを先に実行する」原則をセットで設けるのがよいでしょう。

Cluster APIとの比較 — そして共存

Cluster API(CAPI)は、Kubernetesクラスタそのものを Kubernetesリソース(Cluster、MachineDeploymentなど)として宣言し、マネジメントクラスタのコントローラが継続的に状態を収束させるSIGプロジェクトです。Kubesprayとは哲学が正反対です。

観点KubesprayCluster API
パラダイム手続き的 — 実行時のみ収束宣言的 — コントローラが常時収束
必要なインフラAnsibleコントロールノードのみマネジメントクラスタ+インフラプロバイダ
ベアメタル対応SSHが通ればどこでもMetal3(IPMI/Redfish)またはBYOHプロバイダが必要
ノード復旧手動(プレイブック再実行)MachineHealthCheckで自動再作成が可能
多クラスタ運用インベントリの数だけ反復実行フリート管理に強い
OSカスタム非常に柔軟(既存OSの上に適用)イメージビルドのパイプラインが必要
学習曲線Ansible経験者には緩やかCRD・コントローラモデルの理解が必要

判断基準を整理するとこうなります。

  • クラスタが一桁台で、サーバーが手動プロビジョニングされ、組織にAnsibleのスキルがあるなら、Kubesprayがシンプルで十分です。
  • クラスタを数十個量産する必要があり、IPMI/Redfishでベアメタルのライフサイクルまで自動化できるなら、CAPI+Metal3が長期的には優位です。
  • 現実的なハイブリッドもあります。CAPIのマネジメントクラスタ自体は鶏と卵の問題によりどこかでブートストラップされる必要があり、この最初のクラスタをKubesprayで構築し、残りのフリートをCAPIで管理するパターンです。既存のKubesprayクラスタを維持しつつ、新規クラスタからCAPIへ移行する漸進的な移行もよく見られます。

よくある落とし穴とトラブルシューティング

落とし穴リスト

  1. インベントリドリフト: 誰かがノード上で手動設定を変えたり、remove-node後にhosts.yamlを直さなかったりすると、次のプレイブック実行が想定外の動作をします。インベントリ変更はPR経由のみ、ノードの手動変更は禁止という規律が答えです。
  2. OSパッチとの衝突: unattended-upgradesがcontainerdを勝手に上げたり、カーネル更新後の再起動でカーネルモジュール設定が抜けたりする事故は頻発します。Kubernetes関連パッケージはholdで固定し、OSパッチはドレインを伴う統制された手順で実施すべきです。
  3. CNIの変更不可: kube_network_pluginを変えてcluster.ymlを再実行するとクラスタが壊れます。CNIの入れ替えが本当に必要なら、新規クラスタ構築とワークロード移転が定石です。
  4. KubesprayとAnsibleのバージョン不一致: 各リリースは特定のAnsibleバージョン範囲を要求します。リリースごとに仮想環境を作り直し、requirements.txtを再インストールするのをルーチンにしてください。
  5. 別バージョンのタグで既存クラスタにcluster.ymlを実行: 意図しないコンポーネントのアップグレードが発生します。クラスタごとのバージョン対応表が重要な理由です。
  6. NTPのずれ: ノード間の時刻がずれると、証明書検証とetcdが不安定になります。chronyの構成をpreinstall段階の点検リストへ含めてください。

失敗時の再開とログ

# 詳細ログで再実行 — 冪等性により完了済みタスクはchangedなしで通過
ansible-playbook -i inventory/prod/hosts.yaml --become cluster.yml -vvv \
  2>&1 | tee /tmp/kubespray-run.log

# ノード側の一次確認ポイント
journalctl -u kubelet -f          # kubelet起動失敗の原因
journalctl -u containerd -f      # ランタイム/レジストリの問題
crictl ps -a                      # コントロールプレーンのスタティックPod状態
ls /etc/kubernetes/manifests/     # kubeadmスタティックPodマニフェスト

経験的に、失敗の8割は「特定ノードの環境差異」です。失敗したタスク名とノードを確認し、そのノードで同じ操作を手動で再現すると原因が早く浮かび上がります。解決後はプレイブック全体を最初から再実行し、収束状態を確認するのが安全です。

プロダクションチェックリスト

[ ] インベントリ・group_varsがGitでPRレビュー管理されているか
[ ] Kubesprayリリースタグとkube_versionの対応が文書化されているか
[ ] CP3台+etcdクォーラム、etcdはSSD/NVMeか
[ ] APIサーバーVIP(kube-vipまたはHAProxy+keepalived)のフェイルオーバーテスト合格
[ ] Pod/Service CIDRが社内IPAMに登録され重複がないか
[ ] etcdスナップショットの定期取得+外部搬出+四半期ごとのリストア演習
[ ] auto_renew_certificates有効+証明書失効の監視
[ ] kubernetes_audit有効+中央ログ収集
[ ] kube_encrypt_secret_data有効
[ ] PSAデフォルト(enforce baseline以上)の適用
[ ] kube-bench実行結果と例外理由の文書化
[ ] アップグレード手順(スキップ禁止、serial、PDB点検)がランブック化されているか
[ ] ステージングクラスタで同一変更を先に検証するプロセス
[ ] OSパッチポリシーとKubernetesパッケージのhold設定

おわりに

Kubesprayは華やかなツールではありません。新しい抽象化を発明する代わりに、kubeadmというアップストリーム標準の上にAnsibleという実績ある自動化を重ね、「汎用Linuxサーバーの山をプロダクションKubernetesへ変える」最も現実的な経路を提供します。その対価は、手続き的ツールの宿命である運用規律です。インベントリをGitで統制し、バージョン対応表を維持し、ステージングで先に検証し、バックアップと演習を欠かさない組織にとって、Kubesprayは何年も安定して働いてくれる道具です。

逆に、クラスタ数が増えてフリート管理が本質的な課題になる時点が来たら、Cluster APIへの移行を検討すべきタイミングです。そのときでさえ、最初のマネジメントクラスタをブートストラップする場所には、依然としてKubesprayがいる可能性が高いでしょう。オンプレKubernetesを始めるチームには、kubeadmを一度手動で最後までやり切ってからKubesprayで自動化する学習経路をお勧めします。ツールが何を肩代わりしてくれているのかを知って初めて、ツールが失敗したときに自分の手で直せるからです。

参考資料