KubernetesのCustom Resourceを試した
KubernetesのCustom ResourceはKubernetesをユーザが拡張するためのものです。
Kubernetesの標準にあるDeploymentのようなリソースをユーザが独自に作成し、リソースの定義に応じて、Kubernetes上のリソースをコントロールすることが可能です。
ControllerとResourceについて
Custom Resourceを学ぶにあたり、Kubernetesの仕組みを理解する必要があります。
ControllerとResourceというものがKubernetesでは機能していて、これがCustom Resourceのベースになっています。
まずはこれらをざっと理解しておくとCustom Resourceの理解がスムーズになります。
こちらの資料がとても参考になりました。
Custom ControllerとCustom Resourceを作る
ControllerとResourceは一体です。
Custom Resourceというユーザ独自のリソースを作るにあたり、Custom ControllerというCustom Resource用のControllerを用意する必要があります。
今回はKubebuilderというCustom Resource、Custom Controllerを作成するためのSDKを利用します。
なお、Custom Resource、Custom Controllerは素の状態でも作成することが可能ですが、ControllerとResourceの仕組みに則って作成する必要があり、なかなか複雑な作業になります。
Custom Controllerの例としてsample controllerというものが公式で公開されています。これと同内容のものを作成します。
deploymentのリソースがセルフヒーリングするサンプルになっています。
Kubebuilderをインストールする
Kubebuilderを下記のドキュメントの通りにインストールします。
Quick Start - The Kubebuilder Book
プロジェクトを作成する
$ cd go/src/tutorial.kubebuilder.io $ kubebuilder init --domain my.domain
APIを作成する
$ kubebuilder create api --group webapp --version v1 --kind MyKind Create Resource [y/n] y Create Controller [y/n] y
Custom Resourceを修正する
api/v1/mykind_types.goを修正します。
このファイルの定義内容がcustom resourceのyamlの定義に反映されます。
mykind_types.go
package v1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // MyKindSpec defines the desired state of MyKind type MyKindSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file // DeploymentName is the name of the Deployment resource that the // controller should create. // This field must be specified. // +kubebuilder:validation:MaxLength=64 DeploymentName string `json:"deploymentName"` // Replicas is the number of replicas that should be specified on the // Deployment resource that the controller creates. // If not specified, one replica will be created. // +optional // +kubebuilder:validation:Minimum=0 Replicas *int32 `json:"replicas,omitempty"` } // MyKindStatus defines the observed state of MyKind type MyKindStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file // ReadyReplicas is the number of 'ready' replicas observed on the // Deployment resource created for this MyKind resource. // +optional // +kubebuilder:validation:Minimum=0 ReadyReplicas int32 `json:"readyReplicas,omitempty"` } // +kubebuilder:object:root=true // MyKind is the Schema for the mykinds API type MyKind struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec MyKindSpec `json:"spec,omitempty"` Status MyKindStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true // MyKindList contains a list of MyKind type MyKindList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []MyKind `json:"items"` } func init() { SchemeBuilder.Register(&MyKind{}, &MyKindList{}) }
goのstructを使ってcustom resourceのデータ構造を定義します。
kubernetesでは、項目名はcamelcaseがルールなので、structのjson tagを利用して項目名を変換します。
kubebuilderのmetadataを使って、validationなどの設定が可能です。
詳細は以下にあります。
Markers for Config/Code Generation - The Kubebuilder Book
Custom Controllerを修正する
controllers/mykind_controller.goを修正します。
mykind_controller.go(前半部分)
package controllers import ( "context" apps "k8s.io/api/apps/v1" core "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/record" "github.com/go-logr/logr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" webappv1 "tutorial.kubebuilder.io/sample-controller/api/v1" ) // MyKindReconciler reconciles a MyKind object type MyKindReconciler struct { client.Client Log logr.Logger Recorder record.EventRecorder } // +kubebuilder:rbac:groups=webapp.my.domain,resources=mykinds,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=webapp.my.domain,resources=mykinds/status,verbs=get;update;patch // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;delete // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch func (r *MyKindReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ctx := context.Background() log := r.Log.WithValues("mykind", req.NamespacedName) // your logic here log.Info("fetching MyKind resource") myKind := webappv1.MyKind{} if err := r.Client.Get(ctx, req.NamespacedName, &myKind); err != nil { log.Error(err, "failed to get MyKind resource") // Ignore NotFound errors as they will be retried automatically if the // resource is created in future. return ctrl.Result{}, client.IgnoreNotFound(err) } if err := r.cleanupOwnedResources(ctx, log, &myKind); err != nil { log.Error(err, "failed to clean up old Deployment resources for this MyKind") return ctrl.Result{}, err } log = log.WithValues("deployment_name", myKind.Spec.DeploymentName) log.Info("checking if an existing Deployment exists for this resource") deployment := apps.Deployment{} err := r.Client.Get(ctx, client.ObjectKey{Namespace: myKind.Namespace, Name: myKind.Spec.DeploymentName}, &deployment) if apierrors.IsNotFound(err) { log.Info("could not find existing Deployment for MyKind, creating one...") deployment = *buildDeployment(myKind) if err := r.Client.Create(ctx, &deployment); err != nil { log.Error(err, "failed to create Deployment resource") return ctrl.Result{}, err } r.Recorder.Eventf(&myKind, core.EventTypeNormal, "Created", "Created deployment %q", deployment.Name) log.Info("created Deployment resource for MyKind") return ctrl.Result{}, nil } if err != nil { log.Error(err, "failed to get Deployment for MyKind resource") return ctrl.Result{}, err } log.Info("existing Deployment resource already exists for MyKind, checking replica count") expectedReplicas := int32(1) if myKind.Spec.Replicas != nil { expectedReplicas = *myKind.Spec.Replicas } if *deployment.Spec.Replicas != expectedReplicas { log.Info("updating replica count", "old_count", *deployment.Spec.Replicas, "new_count", expectedReplicas) deployment.Spec.Replicas = &expectedReplicas if err := r.Client.Update(ctx, &deployment); err != nil { log.Error(err, "failed to Deployment update replica count") return ctrl.Result{}, err } r.Recorder.Eventf(&myKind, core.EventTypeNormal, "Scaled", "Scaled deployment %q to %d replicas", deployment.Name, expectedReplicas) return ctrl.Result{}, nil } log.Info("replica count up to date", "replica_count", *deployment.Spec.Replicas) log.Info("updating MyKind resource status") myKind.Status.ReadyReplicas = deployment.Status.ReadyReplicas if r.Client.Status().Update(ctx, &myKind); err != nil { log.Error(err, "failed to update MyKind status") return ctrl.Result{}, err } log.Info("resource status synced") return ctrl.Result{}, nil }
Reconcile関数はCustom Resourceがapplyされて変更された時に実行される関数になります。
ここにLogicを書いていきます。
Client
でResourceを操作することが可能です。
GetでResourceを取得します。
CreateでResourceを作成します。
UpdateでResourceを更新します。
mykind_controller.go(後半部分)
// cleanupOwnedResources will Delete any existing Deployment resources that // were created for the given MyKind that no longer match the // myKind.spec.deploymentName field. func (r *MyKindReconciler) cleanupOwnedResources(ctx context.Context, log logr.Logger, myKind *webappv1.MyKind) error { log.Info("finding existing Deployments for MyKind resource") // List all deployment resources owned by this MyKind var deployments apps.DeploymentList if err := r.List(ctx, &deployments, client.InNamespace(myKind.Namespace), client.MatchingField(deploymentOwnerKey, myKind.Name)); err != nil { return err } deleted := 0 for _, depl := range deployments.Items { if depl.Name == myKind.Spec.DeploymentName { // If this deployment's name matches the one on the MyKind resource // then do not delete it. continue } if err := r.Client.Delete(ctx, &depl); err != nil { log.Error(err, "failed to delete Deployment resource") return err } r.Recorder.Eventf(myKind, core.EventTypeNormal, "Deleted", "Deleted deployment %q", depl.Name) deleted++ } log.Info("finished cleaning up old Deployment resources", "number_deleted", deleted) return nil } func buildDeployment(myKind webappv1.MyKind) *apps.Deployment { deployment := apps.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: myKind.Spec.DeploymentName, Namespace: myKind.Namespace, OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(&myKind, webappv1.GroupVersion.WithKind("MyKind"))}, }, Spec: apps.DeploymentSpec{ Replicas: myKind.Spec.Replicas, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "deployment-name": myKind.Spec.DeploymentName, }, }, Template: core.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ "deployment-name": myKind.Spec.DeploymentName, }, }, Spec: core.PodSpec{ Containers: []core.Container{ { Name: "nginx", Image: "nginx:latest", }, }, }, }, }, } return &deployment } var ( deploymentOwnerKey = ".metadata.controller" ) func (r *MyKindReconciler) SetupWithManager(mgr ctrl.Manager) error { if err := mgr.GetFieldIndexer().IndexField(&apps.Deployment{}, deploymentOwnerKey, func(rawObj runtime.Object) []string { // grab the Deployment object, extract the owner... depl := rawObj.(*apps.Deployment) owner := metav1.GetControllerOf(depl) if owner == nil { return nil } // ...make sure it's a MyKind... if owner.APIVersion != webappv1.GroupVersion.String() || owner.Kind != "MyKind" { return nil } // ...and if so, return it return []string{owner.Name} }); err != nil { return err } return ctrl.NewControllerManagedBy(mgr). For(&webappv1.MyKind{}). Owns(&apps.Deployment{}). Complete(r) }
SetupWithManager関数でReconcile関数が呼ばれるように設定します。
cleanupOwnedResources関数で既存のCustom ResourceがDeploymentを作成していた場合にそれを削除します。
buildDeployment関数でDeploymentリソースを作成します。
起動処理を修正する
main.goを修正します。
main.go
package main import ( "flag" "os" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/log/zap" webappv1 "tutorial.kubebuilder.io/sample-controller/api/v1" "tutorial.kubebuilder.io/sample-controller/controllers" // +kubebuilder:scaffold:imports ) var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") ) func init() { _ = clientgoscheme.AddToScheme(scheme) _ = webappv1.AddToScheme(scheme) // +kubebuilder:scaffold:scheme } func main() { var metricsAddr string var enableLeaderElection bool flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") flag.Parse() ctrl.SetLogger(zap.Logger(true)) mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, MetricsBindAddress: metricsAddr, LeaderElection: enableLeaderElection, Port: 9443, }) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } if err = (&controllers.MyKindReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("controllers").WithName("MyKind"), Recorder: mgr.GetEventRecorderFor("mykind-controller"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "MyKind") os.Exit(1) } // +kubebuilder:scaffold:builder setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } }
Recorderをcontrollerに渡すようにして、Custom Resourceのイベントを記録するようにします。
Custom Resourceをkubernetes環境にインストールする
$ make install
Custom Controllerを動かす
$ make run
結果を確認する
Custom Resourceを以下のように定義します。
webapp_v1_mykind.yaml
apiVersion: webapp.my.domain/v1 kind: MyKind metadata: name: mykind-sample spec: # Add fields here foo: bar deploymentName: mykind replicas: 1
custom resourceを作成します。
$ kubectl apply config/samples/webapp_v1_mykind.yaml
しばらくするとdeploymentが作られます。
$ kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE mykind 1/1 1 1 33s
deploymentを削除してもしばらくするとまた同じdeploymentが復活します。
$ kubectl delete deployments mykind
まとめ
公式のドキュメントは内容が難しいと思います。
個人の方がCustom Resourceについてまとめているドキュメントを参考にしながらCustom Resourceを理解していきました。
Custom ResourceはKubernetes環境をより便利にできる機能です。
Kubebuilderを使うとより楽にCustom Resourceを準備することができます。
ただこれを使って何をするのかは分かっていません。Kubernetesを使っている人たちはどのようにCustom Resourceを使っているのかを知っていく必要があると思います。