Hosted Control Plane構成のKubernetesクラスタ構築


この記事はkubernetes Advent Calendar 2025 の 21 日目の記事です。


まとめ

自宅サーバーのKubernetes環境を、VM主体の構成からベアメタル主体へ移行するにあたり、コントロールプレーンを別クラスタ上に配置するHosted Control Plane構成を検証した。 Cluster APIを用い、k0smotronをBootstrap/Control Plane/Infrastructure Providerとして採用し、GKE AutopilotをManagement Cluster、VPN接続した自宅ベアメタルをワーカーノードとする構成を構築した。Provider間の依存関係により想定通り組み合わせられないケースに注意が必要だったり、PXE boot整備の手間などの課題はあるが、柔軟な構成を実現できることを実証できた。

経緯

自宅サーバー上のKubernetesクラスタについて、今までKVMなどで立ち上げた仮想マシンをノードとして構築していたが、 大抵のワークロードをKubernetesクラスタ上にデプロイするようになり、Kubernetesノードと関係のないVMを利用したいことが少なくなっているので、 思い切ってベアメタルKubernetesクラスタに移行してみようと思い立った。

その際、コントロールプレーンノードについてはクラウドサービス(KaaS)上に構築してみることにした。 自宅環境は停電などで全停止することも珍しくないため、コントロールプレーンだけでも外に出しておきたいと考えたのと、 コントロールプレーン用のノードを用意するには物理サーバーがもったいなかったことと、あとは単なる興味があったのが理由だ。

今回そのような方針で構築するにあたって、Hosted Control Planeと呼ばれる構成を取り入れてみた。

Hosted Control Plane

概要

Kubernetesクラスタの構成要素として、まずクラスタにデプロイされたコンテナを稼働させるためのKubeletがインストールされたノードが必要になる。 それに加えて、Kubernetes API、そのバックエンドストレージとなるetcd、Podをどのノードに配置するかを決定するスケジューラーなども必要となり、これらは総じてコントロールプレーンと呼ばれている。 自宅環境やオンプレミス環境でKubernetesクラスタを構築する場合、コントロールプレーンはそれぞれクラスタ内のノード上にPodとしてデプロイする構成がおそらく一般的だと思う。

しかし、これらのコントロールプレーンをクラスタを構成するノードの外で稼働させる構成を取ることもでき、これはHosted Control Planeと呼ばれている。 Kubernetesのドキュメントの中 でも触れられているように、 マネージドKubernetesサービスや、旧来の構成は同様にコントロールプレーンをクラスタのノード外に持つ構成となるが、 ここでは特に他のKubernetesクラスタ上でコントロールプレーンを動かす構成に注目する。

一般的な構成

architecture-beta group k8s[Kubernetes Cluster] group node1[ControlPlaneNode] in k8s service cp1(server)[control plane services] in node1 service etcd1(server)[etcd] in node1 service kl1[kubelet] in node1 group node2[WorkerNode] in k8s service kl2[kubelet] in node2 service o(server)[pods] in node2 cp1:T -- B:etcd1 kl1:L --> R:cp1 kl2:T --> B:cp1

Hosted Control Plane

architecture-beta group mngk8s[Other Kubernetes Cluster] service cp1(server)[control plane services] in mngk8s service etcd1(server)[etcd] in mngk8s group k8s[Kubernetes Cluster] group node2[WorkerNode] in k8s service kl2[kubelet] in node2 service o(server)[pods] in node2 cp1:T -- B:etcd1 kl2:T --> B:cp1

対応するCluster API Provider

Cluster API を利用すれば、Kubernetesクラスタの構築を自動化できるが、その中でHosted Control Plane構成を取ることもできる。 Cluster APIは様々な構成に対応できるよう、Providerと呼ばれるモジュールがいくつも存在しており、Hosted Control Plane構成に対応するControl Plane Providerもいくつかある。 例えば、以下が挙げられる。

他にもあるかもしれないが、簡単に調べた範囲ではこの3つが見つかった。ただし、3つとも検証したわけではない。

Cluster APIの注意点

Cluster APIのProviderは、役割に応じて以下の3つの種類がある。

これらの詳細は、Cluster API の Contract で規定されており、 設計思想としては Bootstrap/Control Plane/Infrastructure Providerのそれぞれを独立して選び、組み合わせて使うことができる。

ただし、実際には完全に独立して組み合わせて使えるとは限らず、特定の組み合わせでないと動かないProviderも存在する。 例えば、Infrastructure ProviderのSideroは、ベアメタルサーバーをノードとして利用するために使えるProviderだが、 Talos Linuxと密接に依存しており、ControlPlane ProviderにもTalosを採用する必要がある。 (ノードがクラスタにjoinする際に、コントロールプレーンで稼働するtrustdというTalos固有のコンポーネントを利用するため、他のコントロールプレーンと組み合わせてもノードがクラスタに参加できない。)

今回最初にSideroを試してそうした点に気づいたが、事前に気づくのはなかなか難しく、構想中の構成が機能するかは実際に検証して判断したほうが良い点には注意が必要となる。

自宅環境のセットアップ例

構成

以下のCluster API Providerの組み合わせで構築してみた。

k0smotronは、k0sというKubernetesディストリビューションを使ってKuberntesクラスタを構築する。 k0smotronはInfrastructure Providerも提供しており、これを使うと、ベアメタル含め好きなLinuxホストへのSSHの接続情報を含むカスタムリソースを作成することで、 そのホストをKubernetesノードとしてセットアップしてくれる。ベアメタルを扱うInfrastructure ProviderにはMetal3もあるが、 ドキュメントを見る限りではIPMIなどのBMCを前提にしているところがあり、BMCを持たないホストもある自宅環境にはマッチしないと判断した。

(ちなみに、k0smotronのInfrastructure Providerは、cloud-init用の形式で用意されたBootstrapConfigを、cloud-initにわたすのではなくProvider側で解釈し、 同等の操作をSSH経由で実行する実装になっているようで、cloud-initで指定できるうちの一部の設定だけに対応している様子だった。 そのため、他のBootstrap Providerと使う場合は相性問題が起こりやすいかもしれない。)

Cluster APIの稼働環境であり、構築するクラスタのコントロールプレーンのデプロイ先にもなるクラスタは、GKEのAutopilotクラスタにした。 自宅内のノードとなるサーバーがつながるネットワークとGKEのクラスタの間は、VPNを介して双方向に接続できるようにした。 また、構築が極力簡単になるよう、自宅内のサーバーはPXE bootを起点にiPXEを介してFlatcar Container Linuxが起動するように整え、 そのために必要なiPXEスクリプトやignitionファイルもGKEのクラスタ上にHTTPサーバーを立てて配布した。

構築手順

PXE bootの設定等に関する部分は本題ではないので、省略する。 GKEのクラスタがいわゆるManagement Clusterとして機能する形であり、そちらにCluster APIをセットアップしてからマニフェストを適用するだけなので、手順そのものはシンプルである。

まずManagement ClusterにCluster APIをインストールする。clusterctlを利用して実施しても良いが、ここではCluster API Operatorを利用した。

helm upgrade --install capi-operator capi-operator/cluster-api-operator --create-namespace -n capi-operator-system \
  --set core.cluster-api.enabled=true --set core.cluster-api.version=v1.11.3 \
  --set bootstrap.k0smotron.enabled=true  --set bootstrap.k0smotron.version=v1.8.1 --set bootstrap.k0smotron.fetchConfig.url=https://github.com/k0sproject/k0smotron/releases/latest/bootstrap-components.yaml \
  --set controlPlane.k0smotron.enabled=true --set controlPlane.k0smotron.version=v1.8.1 --set controlPlane.k0smotron.fetchConfig.url=https://github.com/k0sproject/k0smotron/releases/latest/control-plane-components.yaml \
  --set infrastructure.k0smotron.enabled=true --set infrastructure.k0smotron.version=v1.8.1  --set infrastructure.k0smotron.fetchConfig.url=https://github.com/k0sproject/k0smotron/releases/latest/infrastructure-components.yaml

(これを試した時点では、cluster-api-operatorのChartの問題で、versionを指定せずにfetchConfigを指定するとHelmが生成するManifestが不正な形式になってしまったので、versionを直接指定している。)

その後、以下のようなManifestを適用することで、Managementクラスタ内にk0sによるコントロールプレーンがPodとして作成され、 指定したベアメタルサーバーにk0sがインストールされノードとして利用できるようになる。

apiVersion: v1
kind: Namespace
metadata:
  name: my-cluster
---
apiVersion: cluster.x-k8s.io/v1beta2
kind: Cluster
metadata:
  name: my-cluster
  namespace: my-cluster
spec:
  clusterNetwork:
    pods:
      cidrBlocks:
        - 10.101.0.0/16
    services:
      cidrBlocks:
        - 10.100.0.0/16
  controlPlaneRef:
    apiGroup: controlplane.cluster.x-k8s.io
    kind: K0smotronControlPlane
    name: my-cluster-cp
  infrastructureRef:
    apiGroup: infrastructure.cluster.x-k8s.io
    kind: RemoteCluster
    name: my-cluster-rc
---
apiVersion: controlplane.cluster.x-k8s.io/v1beta1
kind: K0smotronControlPlane
metadata:
  name: my-cluster-cp
  namespace: my-cluster
spec:
  version: v1.34.2
  persistence:
    type: emptyDir
  service:
    type: LoadBalancer
    annotations:
      networking.gke.io/load-balancer-type: "Internal"
    apiPort: 6443
    konnectivityPort: 8132
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: RemoteCluster
metadata:
  name: my-cluster-rc
  namespace: my-cluster
spec:
---
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: K0sWorkerConfigTemplate
metadata:
  name: my-cluster-worker
  namespace: my-cluster
spec:
  template:
    spec:
      version: v1.34.2+k0s.0
      k0sInstallDir: /opt/k0s  # Flatcar Container Linuxだと `/usr` がRead-onlyなため、パス変更が必要
---
apiVersion: cluster.x-k8s.io/v1beta2
kind: MachineDeployment
metadata:
  name: my-cluster-workers
  namespace: my-cluster
spec:
  clusterName: my-cluster
  replicas: 1
  selector:
    matchLabels: {}
  template:
    spec:
      bootstrap:
        configRef:
          apiGroup: bootstrap.cluster.x-k8s.io
          kind: K0sWorkerConfigTemplate
          name: my-cluster-worker
      clusterName: my-cluster
      infrastructureRef:
        apiGroup: infrastructure.cluster.x-k8s.io
        kind: RemoteMachineTemplate
        name: my-cluster-worker
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: RemoteMachineTemplate
metadata:
  name: my-cluster-worker
  namespace: my-cluster
spec:
  template:
    spec:
      pool: default
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: PooledRemoteMachine
metadata:
  name: my-cluster-worker-0
  namespace: my-cluster
spec:
  pool: default
  machine:
    address: 172.31.249.254  # ベアメタルサーバーのIPアドレス
    port: 22
    user: core
    useSudo: true
    sshKeyRef:
      name: sshkey-my-cluster-worker-0
---
apiVersion: v1
kind: Secret
metadata:
  name: sshkey-my-cluster-worker-0
  namespace: my-cluster
type: Opaque
data:
  value: LS0...  # SSH秘密鍵をBase64エンコードしたもの

所感と課題