Cluster API の Infrastructure Provider を実装してみよう
この記事は 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 で公開しています。 本記事とあわせて参考にしていただければ幸いです。
Cluster API
Cluster API は Kubernetes クラスタのライフサイクル (作成・スケール・アップグレード・削除) にかかわる操作を、 Kubernetes の宣言的な API によって提供するものです。 SIG Cluster Lifecycle のプロジェクトとして開発が進められています。
何ができるようになるのか簡単にまとめますと、「クラスタの仕様を YAML で書いて kubectl apply すると、新しい Kubernetes クラスタが作られる」ということができるようになります。 Pod を作るときと同じやり方でクラスタ自体を作ってしまう、というイメージです。
Cluster API のコンポーネントがインストールされている管理用のクラスタを「Management Cluster」といいます。 Cluster API によってライフサイクルを管理されているワークロード用のクラスタは「Target Cluster」と呼ばれます。
クラスタやノードの仕様は、 Cluster や Machine といったカスタムリソースとして管理されます。 このリソースオブジェクトを 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 Kubeadm は kubeadm を使った 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 向けのプロバイダーをつくっていきましょう。
VultrCluster
と VultrMachine
リソースを使えるようにし、シングルマスタのクラスタが起動できる状態を目指します。
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.ready
と status.apiEndpoints
が required なフィールドとなっているため、この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.providerID
と status.ready
が required なフィールドとなっているため、この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さんの記事でも紹介されています👀
ということで、 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
-
以前は各クラウド環境向けのバイナリがビルドされていました。 ↩︎
-
本記事の執筆時点では、すべての開発ガイドが公開状態にはなっていないようでした。GitHub 上には Markdown ファイルが存在するので、自分でビルドして閲覧可能です。https://github.com/kubernetes-sigs/cluster-api/tree/master/docs/book/src/providers ↩︎
-
あらかじめパッケージをインストールした状態のイメージを作成し、それを使ってインスタンスを起動する場合、この手順は不要です。 ↩︎
-
この状態では、 Taint を許容する Pod マニフェストを書かないと、そのノードへは Pod のスケジューリングが行われません。 ↩︎
-
Service を
type: LoadBalancer
で作ったときにロードバランサを作ってくれるのはこの子です。 ↩︎ -
kubectl edit
で Node オブジェクトを直接編集してもよいのですが… 毎回手でやるのはトイルですよね…! ↩︎
Previous Post
Next Post