この記事は Kubernetes Advent Calendar 2019 その2 の16日目の記事になります。 15日目は hanakara_milk さんの NutanixのCSI Volume Plug-inの裏側にあるストレージについて でした!


こんにちは!🎅

Cluster API の動作を詳しく知るために、 Infrastructure Provider の自作にチャレンジしてみました。

この記事では、 Kubebuilder をつかって Vultr 向けの Infrastructure Provider を実装し、 Kubernetes クラスタが利用可能になるまでの様子を順に紹介します。

ついでに Cloud Controller Manager も実装し、 Vultr API が提供するサーバー情報をもとに Node オブジェクトを初期化できるようにします。

成果物は GitHub で公開しています。 本記事とあわせて参考にしていただければ幸いです。

yukirii/cluster-api-provider-vultr
The Vultr provider implementation for Kubernetes Cluster API (CAPI) - yukirii/cluster-api-provider-vultr
yukirii/cloud-provider-vultr
The Vultr provider implementation for Kubernetes Cloud Controller Manager (CCM) - yukirii/cloud-provider-vultr

Cluster API

Cluster API は Kubernetes クラスタのライフサイクル (作成・スケール・アップグレード・削除) にかかわる操作を、 Kubernetes の宣言的な API によって提供するものです。 SIG Cluster Lifecycle のプロジェクトとして開発が進められています。

何ができるようになるのか簡単にまとめますと、「クラスタの仕様を YAML で書いて kubectl apply すると、新しい Kubernetes クラスタが作られる」ということができるようになります。 Pod を作るときと同じやり方でクラスタ自体を作ってしまう、というイメージです。

Cluster API のコンポーネントがインストールされている管理用のクラスタを「Management Cluster」といいます。 Cluster API によってライフサイクルを管理されているワークロード用のクラスタは「Target Cluster」と呼ばれます。

クラスタやノードの仕様は、 ClusterMachine といったカスタムリソースとして管理されます。 このリソースオブジェクトを Cluster API のコントローラーが読み取り、必要な情報の生成やクラウド環境への VM 構築によってクラスタを管理します。 この仕組みには、 CustomResourceDefinitions (CRDs) と、それに対応するカスタムコントローラーによって実現されています。

Cluster API Provider

Cluster API v1alpha2 から、環境依存の CRDs とコントローラーは「プロバイダー」という形で Cluster API 本体から切り出されるようになりました1

v1alpha2 では2種類のプロバイダーが使われます。

  • Infrastructure Provider
    • クラスタを動かすインフラ環境に固有のプロビジョニング処理を行う
      • VPC, LB, VM などのクラウド上のリソースを作成/削除
    • 主要なクラウドサービス向けのプロバイダーが公開されている
  • Bootstrap Provider
    • Kubernetes コンポーネントをサーバーにインストールし、ノードとして初期化する手段を提供する
    • Bootstrap Provider Kubeadmkubeadm を使った cloud-init のコードを生成する
      • 生成されたコードは Infrastructure Provider が VM を起動する際に User Data として使われる

それぞれのプロバイダは、自身が担当するカスタムリソースオブジェクトを読み取り、必要なアクションの実行・ステータスの変更を行います。

Cluster API 本体と環境固有のコードが分離されていることで、お互いの仕様やリリースサイクルに依存することなく、自由に開発をすすめられるというメリットがあります。

まだサポートされていないクラウドサービスで Cluster API を使いたい場合には、 Infrastructure Provider を実装することになります。 また、 OpenShift などの Kubernetes ディストリビューションを使いたい場合も、 Bootstrap Provider を開発し VM プロビジョニングのためのコードを生成することで対応できるようになります。

Vultr

Vultr は海外の VPS サービスです。 東京にもリージョンがあり API も提供されています。

ちょうど知人からいただいたクーポンも持っていたのと、シンプルに使い始められる点が今回のネタにちょうど良いかなと思って選んでみました。

が、ロードバランサがありませんでしたw

あくまでも VPS サービスという位置づけなのですね。 (着手しはじめてから気がつきました…w)

ということで、一旦シングルマスタ構成のクラスタを立ち上げることを目標にしました。

cluster-api-provider-vultr の実装

それでは Vultr 向けのプロバイダーをつくっていきましょう。 VultrClusterVultrMachine リソースを使えるようにし、シングルマスタのクラスタが起動できる状態を目指します。

Infrastructure Provider の満たすべき仕様は、The Cluster API Book2プロポーザルドキュメント に記載されています。 これらの情報を参考にして、必要なリソースとコントローラーを実装していきます。 API バージョンは v1alpha2 を対象とします。

開発環境

  • Go v1.13
  • Kubebuilder v2.2.0
  • Docker v19.03.5
  • Kind v0.6.0
  • Kustomize v3.4.0

Kubebuilder プロジェクトの作成

プロジェクト用ディレクトリを作成し kubebuilder init コマンドで初期化します。

mkdir $GOPATH/src/cluster-api-provider-vultr
cd $GOPATH/src/cluster-api-provider-vultr

kubebuilder init --domain cluster.x-k8s.io

VultrCluster API の追加

kubecuilder create api コマンドで VultrCluster API を追加します。 Create Resource [y/n]Create Controller [y/n] と聞かれるので、両方とも y を入力し、リソースとコントローラーの雛形を作ります。

kubebuilder create api --group infrastructure \
  --version v1alpha2 --kind VultrCluster

次に VultrCluster API で必要となるフィールドを追加します。 編集するファイルは api/v1alpha2/vultrcluster_types.go です。

VultrClusterSpec には、 Vultr 環境上の Kubernetes クラスタが満たすべき仕様を定義します。 ここでは、Vultr のリージョン (DCID) を表す Region フィールドを追加しました。

VultrClusterStatus には、 Kubernetes クラスタの状態を保持するためのフィールドを定義します。

Infrastructure Provider の Cluster オブジェクトは、 status.readystatus.apiEndpointsrequired なフィールドとなっているため、この2つを用意しておきます。

VultrCluster コントローラーの実装

次に VultrCluster コントローラーのコードを書いていきます。 編集するファイルは controllers/vultrcluster_controller.go です。

Reconcile() 関数では、 VultrCluster オブジェクトを取得し、状況に応じた reconcile 処理を呼び出しています。

reconcileCluster() 関数は、 VultrCluster オブジェクトが作成・更新された際の処理を実行します。 この関数では、 Finalizer 設定と、 VultrClusterSpec の内容に基づき Kubernetes クラスタに必要なインフラ環境の構築処理を行います。

本来ここでは、 control-plane 用のロードバランサを作成し APIEndpoints の一覧に登録するという処理が行われます。 Vultr ではロードバランサ機能が提供されていないため、代わりに control-plane 用の Reserved IP を作成し VultrCluster.Status.APIEndpoints に追加する処理を行っています。

reconcileClusterDelete() 関数は VultrCluster オブジェクトが削除される際に実行されます。 この関数では、 VultrCluster.Status.APIEndpoints に登録された Reserved IP を削除し、 Finalizer を削除しています。

VultrMachine API の追加

VultrCluster と同様に kubecuilder create api コマンドで VultrMachine API を追加します。 こちらも、リソースとコントローラーの雛形を作成しておきます。

kubebuilder create api --group infrastructure \
  --version v1alpha2--kind VultrMachine

次に VultrMachine API で必要となるフィールドを追加していきます。 編集するファイルは api/v1alpha2/vultrmachine_types.goです。

VultrMachineSpec には、 Kubernetes ノードとして使う Server インスタンスの設定を定義します。 使用する OS やサーバーのスペックを指定できるようにしています。

VultrMachineStatus には、 Vultr Server インスタンスの状態を保持するためのフィールドを定義します。

Infrastructure Provider の Machine オブジェクトは、 spec.providerIDstatus.readyrequired なフィールドとなっているため、この2つを用意しておきます。

VultrMachine コントローラーの実装

次に VultrMachine コントローラーのコードを書いていきます。 編集するファイルは controllers/vultrmachine_controller.go です。

reconcile の基本的な流れは VultrCluster の場合と似ています。 Reconcile() 関数VultrMachine オブジェクトを取得し、状況に応じた reconcile 処理を呼び出しています。

reconcileNormal() 関数は、 VultrMachine オブジェクトが作成・更新された際の処理を実行します。

インスタンスの作成は getOrCreate() 関数で行われます。 Machine.Spec.Bootstrap.Data に Bootstrap Provider によって生成された cloud-init のコードが格納されているので、これを取り出し User Data としてインスタンスの起動オプションに追加しています。

reconcileDelete() 関数VultrMachine オブジェクトが削除される際に実行されます。

動作確認1: Kubernetes クラスタをつくってみる

完成したプロバイダーでクラスタが作られる様子を確認してみます。

Vultr Server 用の Startup Script の作成

起動直後のインスタンスには Kubernetes のパッケージや cloud-init がインストールされていません。 そのため、そのままだと User Data で渡した kubeadm のブートストラップ処理が実行されません。

Startup Script という機能を使うと、サーバー起動時にシェルスクリプトが実行できます。 今回はこの機能を使って、必要なパッケージのインストールと cloud-init の自動実行をさせました3。 スクリプトは Gist で公開しています

Vultr の管理画面から Startup Script 作成すると SCRIPTID が割り当てられるので、これをメモしておきます。

YAML マニフェストを生成する

必要なマニフェストを一括生成するスクリプトをプロバイダーのリポジトリに用意しました。 これを使って Cluster や Machine の YAML マニフェストを用意します。

Vultr の管理画面 から Personal Access Token を取得し、 VULTR_API_KEY 環境変数に値をセットします。 その後 examples/generate.sh スクリプトを実行すると、 _out ディレクトリ配下に YAML マニフェストが生成されます。

_out/provider-components.yaml および _out/addons.yaml には Vultr の API Key が含まれるため、取り扱いには注意してください。

export VULTR_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxx"

git clone https://github.com/yukirii/cluster-api-provider-vultr.git
cd cluster-api-provider-vultr/examples
bash generate.sh

Vultr のリージョンや使用するマシンタイプなどは、generate.sh に記載の環境変数で変更可能です。

1つ前の手順でつくった Startup Script の ID を、 _out/controlplane.yaml_out/machines.yaml に追加しておきます。

apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2
kind: VultrMachine
metadata:
  name: capi-controlplane-0
  namespace: default
spec:
    ...
  scriptID: 1234567     # 追加

Management Cluster を用意する

Kind でローカル環境上に Kubernetes クラスタを立ち上げ、 Management Cluster として使います。

kind create cluster

クラスタが立ち上がったら、_out/provider-components.yaml を apply して Cluster API のコンポーネント一式をインストールします。

kubectl apply -f _out/provider-components.yaml

capi (Cluster API 本体) 、cabpk (Bootstrap Provider Kubeadm) 、 capv (今回つくった Infrastructure Provider Vultr) の3つの Pod が Running になっていれば準備完了です。

% kubectl get pods --all-namespaces | grep -E "ca(pi|bpk|pv)-system"
cabpk-system   cabpk-controller-manager-6d585b4dc4-skdps    2/2     Running            0          2m40s
capi-system    capi-controller-manager-7bb7f74dcb-glmn2     1/1     Running            0          2m40s
capv-system    capv-controller-manager-6d676bbd49-mj29r     1/2     ImagePullBackOff   0          2m40s

Target Cluster を作る

_out/cluster.yaml を apply して、 Cluster と VultrCluster オブジェクトを作成します。

% kubectl apply -f _out/cluster.yaml
cluster.cluster.x-k8s.io/capi created
vultrcluster.infrastructure.cluster.x-k8s.io/capi created
% kubectl get clusters,vultrclusters
NAME                            PHASE
cluster.cluster.x-k8s.io/capi   provisioning

NAME                                                AGE
vultrcluster.infrastructure.cluster.x-k8s.io/capi   33s

Cluster オブジェクトの PHASE が provisioned に変わり、Reserved IPs の中に IP アドレスが増えていれば成功です。

続いて _out/controlplane.yaml を apply し、 control-plane のマシンを作成します。

% kubectl apply -f _out/controlplane.yaml
kubeadmconfig.bootstrap.cluster.x-k8s.io/capi-controlplane-0 created
machine.cluster.x-k8s.io/capi-controlplane-0 created
vultrmachine.infrastructure.cluster.x-k8s.io/capi-controlplane-0 created

% kubectl get machines,vultrmachines,kubeadmconfigs
NAME                                           PROVIDERID   PHASE
machine.cluster.x-k8s.io/capi-controlplane-0                provisioning

NAME                                                               AGE
vultrmachine.infrastructure.cluster.x-k8s.io/capi-controlplane-0   16s

NAME                                                           AGE
kubeadmconfig.bootstrap.cluster.x-k8s.io/capi-controlplane-0   16s

Machine へ PROVIDERID が設定され、 PHASE が provisioned に変わったら、 Vultr の管理画面を見てみましょう。

% kubectl get machines
NAME                  PROVIDERID           PHASE
capi-controlplane-0   vultr:////31970904   provisioned

control-plane 用のインスタンスが立ち上がりました! Startup Script の実行が完了し Kubeadm によるブートストラップが完了したら、 Kubernetes クラスタとしてアクセスできます。

アクセスに必要な Kubeconfig は、 Management Cluster 上に Secret として保存されています。

% kubectl get secret capi-kubeconfig -o jsonpath='{.data.value}' | base64 -D > kubeconfig

KUBECONFIG 環境変数に取得したファイルを指定し、Node の一覧を取得してみます。

% KUBECONFIG=./kubeconfig kubectl get nodes
NAME                  STATUS     ROLES    AGE   VERSION
capi-controlplane-0   NotReady   master   21s   v1.17.0

起動したクラスタにアクセスできました! しかし、やらなければいけないことがあと2つ残っています。

1つ目はネットワークプラグインのインストールです。 Node のステータスが NotReady になっていますが、これはネットワークプラグインがクラスタにインストールされていないためです。

そして2つ目は Node オブジェクトの初期化です。 オブジェクトには、まだ Node が初期化されていないことを表す node.cloudprovider.kubernetes.io/uninitialized が設定されています4

また、 Node オブジェクトには spec.ProviderID フィールドが設定されていません。 通常は Machine と同じ ProviderID を持つ Node が Ready になると、 Cluster API の Machine Controller によって Machine の状態は running に設定されます (こちらのページに状態遷移図が載っています)。

ProviderID フィールドがない場合、 Management Cluster 上の Machine の状態は provisioned のままになってしまいます。 capi-controller-manager のログにも、対応する Node が見つけられないというログが出ています (このあたりの処理)。

% kubectl logs -n capi-system capi-controller-manager-7bb7f74dcb-z7vs7 | tail -n 1
I1215 11:16:01.464505       1 machine_controller.go:164] Reconciliation for
Machine "capi-controlplane-0" in namespace "default" asked to requeue:
cannot assign NodeRef to Machine "capi-controlplane-0" in namespace "default",
no matching Node: requeue in 10s

この Node オブジェクトの初期化には Cloud Controller Manager (CCM) が使われます。 クラウド環境固有の処理を実行し、 Kubernetes とクラウド環境を連携させる役割を持っています5。 CCM 内で動作するコントローラーにはいくつかの種類がありますが、インスタンスが作成・削除された際の Node オブジェクトの操作は NodeController が担います。

CCM については、Kubernetes Advent Calendar 2019 10日目のbells17さんの記事でも紹介されています👀

KubernetesのCloud Controller Managerについて
これはKubernetes Advent Calendar 2019 10日目のエントリーです。 この記事ではKubernetesのCloud Controller Managerについて紹介していきます。

ということで、 Vultr 向けの Cloud Controller Manager も作ってみました6。 実装はそれほど難しくなく、 Cloud Provider Interface を満たすように構造体と関数を実装していけばよいです。 すべての構造体/関数を実装する必要はなく、今回は Node の初期化するのが目標なので Instances のみを実装しています。

今回のサンプルでは、ネットワークプラグイン (Calico) と Cloud Controller Manager のマニフェストを _out/addons.yaml に用意しました。 apply してインストールします。

KUBECONFIG=./kubeconfig kubectl apply -f _out/addons.yaml

さて、Node オブジェクトと Management Cluster 上の Machine オブジェクトの、それぞれのステータスは変わったでしょうか?

% KUBECONFIG=./kubeconfig kubectl get nodes
NAME                  STATUS   ROLES    AGE    VERSION
capi-controlplane-0   Ready    master   157m   v1.17.0

% kubectl get machines
NAME                  PROVIDERID           PHASE
capi-controlplane-0   vultr:////31970904   running

ステータスが変化しました! これでシングルマスタのクラスタ構築が完了です。

動作確認2: ワーカーノードを追加してみる

続いてワーカーノードをクラスタに追加してみましょう。 YAML マニフェストは _out/machines.yaml に用意しています。 Machine および VultrMachine の内容は control-plane とほぼ同様ですが、 KubeadmConfig は既存のクラスタに join するための設定になっている点が異なります。

YAML ファイルを apply し、 Machine と Node が追加されたか確認してみましょう。

kubectl apply -f _out/machines.yaml

Vultr の管理画面上に新しいインスタンスが現れました。

% kubectl get machines,vultrmachines
NAME                                           PROVIDERID           PHASE
machine.cluster.x-k8s.io/capi-controlplane-0   vultr:////31970904   running
machine.cluster.x-k8s.io/capi-node-0           vultr:////31973306   running

NAME                                                               AGE
vultrmachine.infrastructure.cluster.x-k8s.io/capi-controlplane-0   175m
vultrmachine.infrastructure.cluster.x-k8s.io/capi-node-0           4m47s

% KUBECONFIG=./kubeconfig kubectl get nodes
NAME                  STATUS   ROLES    AGE    VERSION
capi-controlplane-0   Ready    master   171m   v1.17.0
capi-node-0           Ready    <none>   84s    v1.17.0

kubectl get コマンドでの結果も問題なさそうです!

動作確認3: Kubernetes クラスタを削除する

ノードやクラスタを片付けるには、作成したオブジェクトを Management Cluster から削除します。 オブジェクト削除時の reconcile 処理が実行され、 Vultr 上のリソースが削除されます。 Vultr の管理画面で、 Server インスタンスと Reserved IP が削除されていることが確認できれば成功です。

最後に Kind で立ち上げたクラスタを削除し、すべての作業は完了です!

kubectl delete -f _out/machines.yaml -f _out/controlplane.yaml
kubectl delete -f _out/cluster.yaml
kind delete cluster

まとめ

この記事では、 Vultr 向けの Infrastructure Provider を実装し、 Cluster API をつかって Kubernetes クラスタをつくる様子を紹介しました。

プロバイダーの中身は CRDs とカスタムコントローラーなので、いわゆる Kubernetes Operator を開発する場合とほぼかわりません。 パブリッククラウドに限らず、プライベートクラウドで内製の API を使っているというケースでも、独自のプロバイダを作って Cluster API に対応させることができるでしょう。

Kubebuilder を使ったカスタムコントローラー開発の勉強ネタにも良いのかなと思います。 公開されてるプロバイダーがどのように実装されているか、コードを読んでみるのも参考になります。


最後まで読んでいただきありがとうございました。

Kubernetes Advent Calendar 2019 その2 はまだまだ続きます。 明日も是非ご覧くださいー!

Kubernetes Advent Calendar 2019 その2


  1. 以前は各クラウド環境向けのバイナリがビルドされていました。 [return]
  2. 本記事の執筆時点では、すべての開発ガイドが公開状態にはなっていないようでした。GitHub 上には Markdown ファイルが存在するので、自分でビルドして閲覧可能です。https://github.com/kubernetes-sigs/cluster-api/tree/master/docs/book/src/providers [return]
  3. あらかじめパッケージをインストールした状態のイメージを作成し、それを使ってインスタンスを起動する場合、この手順は不要です。 [return]
  4. この状態では、 Taint を許容する Pod マニフェストを書かないと、そのノードへは Pod のスケジューリングが行われません。 [return]
  5. Service を type: LoadBalancer で作ったときにロードバランサを作ってくれるのはこの子です。 [return]
  6. kubectl edit で Node オブジェクトを直接編集してもよいのですが… 毎回手でやるのはトイルですよね…! [return]