eyecatch
Fri, Mar 8, 2019

ズンドコキヨシ with Kubernetes Operator - KubebuilderでKubernetes Operatorを作ってみた

Javaの講義、試験が「自作関数を作り記述しなさい」って問題だったから 「ズン」「ドコ」のいずれかをランダムで出力し続けて「ズン」「ズン」「ズン」「ズン」「ドコ」の配列が出たら「キ・ヨ・シ!」って出力した後終了って関数作ったら満点で単位貰ってた — てくも (@kumiromilk) 2016年3月9日 久しぶりにズンドコしたくなったので、Kubebuilderを使って、KubernetesのOperatorとして動くZundoko Operatorを作ってみた。 (adsbygoogle = window.adsbygoogle || []).push({}); Kubernetes Operatorとは KubernetesのOperatorというのはCoreOS社(現Red Hat)によって提唱された概念(実装パターン)で、KubernetesのAPIで登録されるKubernetesオブジェクトの内容に従って何らかの処理をするController (e.g. Deployment Controller)の一種。 Controllerが汎用的なのに対して、特定のアプリケーションに特化しているのが特徴。 アプリケーションごとの細かな設定をKubernetesオブジェクトで表現するために、KubernetesのAPIを拡張する。 APIを拡張するにはAPI Aggregationを使う方法とCustom Resource Definition (CRD)を使う方法がある。 API Aggregationは、Kubernetesオブジェクトをetcd以外で管理したり、WebSocketを使ったり、Kubernetesクラスタ外のAPIサーバを使う場合など、特殊な場合にも対応できる高度なやりかたで、大抵のユースケースではCRDで事足りる。 Operatorも普通はCRDを使う。(というかCRDを使うのがOperatorという人もいる。) CRDとは KubernetesのAPIを簡単に拡張できる仕組みで、Kubernetesオブジェクト(リソース)を定義するKubernetesオブジェクト。 YAMLで、定義したいリソースの名前や型やバリデーションなんかを書いてkubectl applyすれば、そのリソースをKubernetesのREST APIとかkubectlで作成したり取得したりできるようになる。 Operatorの仕組み Operatorは、CRDで定義されたリソース(など)の作成、更新、削除を監視(watch)して、リソースの内容に応じた何らかの処理をするReconciliationループを回すPod。 普通、リソースはOperatorの管理対象のアプリケーションの状態を表す。 で、Operatorはリソースの内容とアプリケーションの状態が同じになるように、Reconciliationループ内でDeploymentを作ったりアプリケーションのAPIを叩いたりする。 ユーザとしては、アプリケーションの構成や設定をKubernetesのAPIで宣言的に統一的に管理できるようになって幸せになれる。 Operator作成ツール Operatorを作るツールとして以下がある。 ツール Operator SDK Kubebuilder Metacontroller 開発元 Kubernetesコミュニティ製 CoreOS社製 GKEチーム製 GitHubスター数 1459 1009 506 開発言語 Go、Ansible、Helm Go 任意 特徴 プロジェクトテンプレート生成、ビルド、デプロイをするCLIツール。AnsibleでもOperatorを書けるのが面白い。Operator FrameworkとしてLifecycle Managerなどが提供されていたり、OperatorHub.ioというコミュニティサイトがあったり、エコシステムが充実している。 プロジェクトテンプレート生成、ビルド、デプロイをするCLIツール。3つの中で一番シンプル。Goでしか開発できない。 他の2つと毛色が違って、Metacontroller自体が汎用的にOperatorを管理するKubernetesアプリ。Operatorの定義をJSONを投げて登録すると、Reconciliationループを回してその中でWebフックを実行してくるので、それを受けて任意の処理をするサーバを任意の言語で書ける。 この中では、Operator SDKが数歩リードしている感じ。 (CoreOS社を買収した)Red Hatが後ろ盾ているし、OperatorHub.ioはGCPとAWSとAzureが協力している。 けど、この記事のネタを書き始めたときにはまだOperatorHub.ioが発表されていなくて、単純にKubebuilderがシンプルでいいと思って採用してしまった。 まあOperator SDKもKubebuilderも下回りのライブラリは同じものを使っているので、だいたい同じだろうし、Operator Frameworkへの移行も難しくなかろう。 Zundoko Operator Kubebuilderで今回作ったのはZundoko Operator。 CRDで定義したリソースは以下。 Hikawa: 作るとズンドコきよしを開始する。 Zundoko: 「ズン」と「ドコ」を表す。Hikawaに管理される。 Kiyoshi: 「キ・ヨ・シ!」を表す。Hikawaに管理される。 Zundoko Operatorは、HikawaとZundokoをwatchする。 Hikawaが作成されると、一定間隔で、ランダムに「ズン」か「ドコ」をセットしたZundokoを作成する。 「ズン」を4つ作ったあとに「ドコ」を作ったら、Kiyoshiを作成して、Zundokoの作成を止める。 Kubebuilderの使い方 Quick Startを参考に。 Kuberbuilderを使うにはGo、depとkustomizeとDockerが必要で、Linuxしかサポートしていない。 自分のPCがWindows 10なので、WSL (Ubuntu 18.04)で環境を作ったんだけど、結局Dockerビルドとかテストとかが上手く動かなかったので、VMとかのLinuxで動かしたほうがよさそう。 Kubebuilderセットアップ Goインストール Goは公式サイトからLinux用アーカイブをダウンロードして展開して、そのbinディレクトリにPATH通すだけでインストールできる。 $ go version go version go1.11.4 linux/amd64 あと、作業ディレクトリを作ってGOPATHを設定しておく。 ~/go/を作業ディレクトリとする。 $ export GOPATH=$HOME/go $ echo 'export GOPATH=$HOME/go' >> ~/.profile $ mkdir $GOPATH/bin $ mkdir $GOPATH/src で、$GOPATH/binにもPATH通しておく。 depインストール Go公式の依存ライブラリ管理ツール。 コマンド一発でインストールできる。 $ curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh kustomizeインストール バイナリをPATHの通ったところにダウンロードするだけ。 $ curl -L https://github.com/kubernetes-sigs/kustomize/releases/download/v2.0.3/kustomize_2.0.3_linux_amd64 -o /usr/local/bin/kustomize $ chmod +x /usr/local/bin/kustomize kubebuilderインストール GitHubのReleasesからアーカイブをダウンロードして展開してPATH通すだけ。 $ version=1.0.6 $ arch=amd64 $ curl -LO https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${version}/kubebuilder_${version}_linux_${arch}.tar.gz $ tar -zxvf kubebuilder_${version}_linux_${arch}.tar.gz $ sudo mv kubebuilder_${version}_linux_${arch} /usr/local/kubebuilder $ export PATH=$PATH:/usr/local/kubebuilder/bin $ echo 'export PATH=$PATH:/usr/local/kubebuilder/bin' >> ~/.profile Dockerインストール は適当に… Kubebuilderプロジェクト生成 Zundoko Operatorのプロジェクトを生成する。 $ mkdir -p $GOPATH/src/github.com/kaitoy/zundoko-operator $ cd $GOPATH/src/github.com/kaitoy/zundoko-operator $ kubebuilder init --owner kaitoy dep ensureを実行するかを聞かれるのでyesで回答すると、依存ライブラリがダウンロードされ、プロジェクトのビルドが走る。 デフォルトではCRDなどの名前空間がk8s.ioになっているので、kaitoy.github.comに変えるべく、zundoko-operator/PROJECTを編集する。 zundoko-operator/PROJECT: version: "1" -domain: k8s.io +domain: kaitoy.github.com repo: github.com/kaitoy/zundoko-operator CRDとController生成 HikawaとZundokoとKiyoshiのCRDを生成する。 $ kubebuilder create api --group zundokokiyoshi --version v1beta1 --kind Hikawa $ kubebuilder create api --group zundokokiyoshi --version v1beta1 --kind Zundoko $ kubebuilder create api --group zundokokiyoshi --version v1beta1 --kind Kiyoshi それぞれ、リソースを作成するか (Create Resource under pkg/apis [y/n]?) と、Controllerを作成するか (Create Controller under pkg/controller [y/n]?) を聞かれる。 リソースはそれぞれ作成して、ControllerはHikawaにだけ作成した。 生成されたのは以下のファイル。 API定義とそのテスト: zundoko-operator/pkg/apis/zundokokiyoshi/v1beta1/*.go CRD: zundoko-operator/config/crds/config/crds/*.yaml Hikawa Controllerとそのテスト: zundoko-operator/pkg/controller/hikawa/*.go リソースのマニフェストのサンプル: zundoko-operator/config/crds/config/samples/*.yaml これらの内、CRDと zundoko-operator/pkg/apis/zundokokiyoshi/v1beta1/zz_generated.deepcopy.go はAPI定義をもとに生成されるので、API定義を書いた後生成しなおすことになる。 API定義記述 リソースがどのような属性をもつかをGoで定義する。 テンプレートは生成されているので、ちょっと書き足すだけでできる。 以下はHikawaのAPI定義。 zundoko-operator/pkg/apis/zundokokiyoshi/v1beta1/hikawa_types.go: package v1beta1 import ( "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // HikawaSpec defines the desired state of Hikawa type HikawaSpec struct { IntervalMillis time.Duration `json:"intervalMillis"` NumZundokos int `json:"numZundokos,omitempty"` SayKiyoshi bool `json:"sayKiyoshi,omitempty"` } // HikawaStatus defines the observed state of Hikawa type HikawaStatus struct { NumZundokosSaid int `json:"numZundokosSaid"` Kiyoshied bool `json:"kiyoshied"` } // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // Hikawa is the Schema for the hikawas API // +k8s:openapi-gen=true type Hikawa struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec HikawaSpec `json:"spec,omitempty"` Status HikawaStatus `json:"status,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // HikawaList contains a list of Hikawa type HikawaList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []Hikawa `json:"items"` } func init() { SchemeBuilder.Register(&Hikawa{}, &HikawaList{}) } 自分で書いたのはHikawaSpecとHikawaStatusの中だけ。 Specの方には期待する状態、Statusの方には現在の実際の状態を表すフィールドを定義するのがパターン。 例えば、HikawaSpec.NumZundokosが期待するZundokoの数で、HikawaStatus.NumZundokosSaidが実際に作成されたZundokono数。 Hikawa ControllerはReconciliationループの中で、SpecとStateが同じになるように処理をすることになる。 ZundokoとKiyoshiのAPI定義は、Specに「Zun」、「Doko」、または「Kiyoshi!」を入れるためのSayフィールドだけを書いた。 Hikawa Controller記述 Hikawa Controllerもテンプレートが生成されているので、それを参考に書ける。 まずはどのリソースをwatchするかを書く。 zundoko-operator/pkg/controller/hikawa/hikawa_controller.go前半抜粋: func add(mgr manager.Manager, r reconcile.Reconciler) error { // Create a new controller c, err := controller.New("hikawa-controller", mgr, controller.Options{Reconciler: r}) if err != nil { return err } // Watch for changes to Hikawa err = c.Watch(&source.Kind{Type: &zundokokiyoshiv1beta1.Hikawa{}}, &handler.EnqueueRequestForObject{}) if err != nil { return err } err = c.Watch(&source.Kind{Type: &zundokokiyoshiv1beta1.Zundoko{}}, &handler.EnqueueRequestForOwner{ IsController: true, OwnerType: &zundokokiyoshiv1beta1.Hikawa{}, }) if err != nil { return err } return nil } Hikawaは普通にwatchして、Zundokoはownしているリソースとしてwatchしている。 Reconciliationループは以下のように書いた。 zundoko-operator/pkg/controller/hikawa/hikawa_controller.go後半抜粋: // +kubebuilder:rbac:groups=zundokokiyoshi.kaitoy.github.com,resources=hikawas,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=zundokokiyoshi.kaitoy.github.com,resources=hikawas/status,verbs=get;update;patch // +kubebuilder:rbac:groups=zundokokiyoshi.kaitoy.github.com,resources=zundokos,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=zundokokiyoshi.kaitoy.github.com,resources=zundokos/status,verbs=get;update;patch // +kubebuilder:rbac:groups=zundokokiyoshi.kaitoy.github.com,resources=kiyoshis,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=zundokokiyoshi.kaitoy.github.com,resources=kiyoshis/status,verbs=get;update;patch func (r *ReconcileHikawa) Reconcile(request reconcile.Request) (reconcile.Result, error) { instanceName := request.NamespacedName.String() log.Info("Reconciling a Hikawa: " + instanceName) // Fetch the Hikawa instance instance := &zundokokiyoshiv1beta1.Hikawa{} err := r.Get(context.TODO(), request.NamespacedName, instance) if err != nil { if errors.IsNotFound(err) { // Object not found, return.