Kubebuilder プロジェクトセットアップ
Kubebuilder を使って実装するカスタムコントローラープロジェクトのセットアップ方法について記載します。
プロジェクト作成
- プロジェクトを初期化します。
# kubebuilder init --domain {APIのドメイン} --repo {リポジトリパス}
kubebuilder init --domain example.nob --repo example.nob/nob-controller
- API オブジェクトおよびコントローラのテンプレートを作成します。
# kubebuilder create api --group {APIグループ} --version {バージョン} --kind {リソース種別}
kubebuilder create api --group nobcontroller --version v1 --kind Nob
実装
Deployment リソースを管理する Nob のコントローラーを実装します。
api/v1/nob_types.go
カスタムリソースの Spec および Status を定義します。変更後は make コマンドを実行して関連するファイルの再生成をしてください。
// NobSpec defines the desired state of Nob
type NobSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
// +kubebuilder:validation:Required
// +kubebuilder:validation:Format:=string
// Nob配下となるDeploymentの名称
DeploymentName string `json:"deploymentName"`
// +kubebuilder:validation:Required
// +kubebuilder:validation:Minimum=0
// Nob配下となるReplicaの数
Replicas *int32 `json:"replicas"`
}
// NobStatus defines the observed state of Nob.
type NobStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
// +optional
// deployment.status.availableReplicasに相当
AvailableReplicas int32 `json:"availableReplicas"`
}
internal/controller/nob_controller.go
Reconcile を行うコントローラー本体を実装します。
- Reconciler の構造体を定義します。
// NobReconciler reconciles a Nob object
type NobReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Recorder record.EventRecorder
}
- RBAC Marker を追記します。
// +kubebuilder:rbac:groups=nobcontroller.example.nob,resources=nobs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=nobcontroller.example.nob,resources=nobs/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=nobcontroller.example.nob,resources=nobs/finalizers,verbs=update
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;delete
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
- SetUpWithManager を定義します。カスタムリソースが他のどのリソースを監視するかが設定されます。
// SetupWithManager sets up the controller with the Manager.
func (r *NobReconciler) SetupWithManager(mgr ctrl.Manager) error {
// NobがDeploymentを監視するよう設定
return ctrl.NewControllerManagedBy(mgr).
For(&nobcontrollerv1.Nob{}).
Owns(&appsv1.Deployment{}). // import appsv1 "k8s.io/api/apps/v1"
Complete(r)
}
- クリーンアップ関数を定義します。Nob リソースへの変更が入るなどして古くなった Deployment を検知して削除します。
// Nob resourceによって作成され、かつnob.spec配下と名前が一致しないDeploymentを削除します。
func (r *NobReconciler) cleanupOwnedResources(
ctx context.Context,
log logr.Logger,
nob *nobcontrollerv1.Nob,
) error {
log.Info("finding existing Deployment for Nob resource")
// Nobが管理するDeploymentを取得
var deployments appsv1.DeploymentList
if err := r.List(
ctx,
&deployments,
client.InNamespace(nob.Namespace),
client.MatchingLabels(map[string]string{"app": "nginx", "controller": nob.Name}),
); err != nil {
return err
}
// nob.spec.deploymentNameに一致しない古いDeploymentを削除
for _, deployment := range deployments.Items {
if deployment.Name == nob.Spec.DeploymentName {
continue
}
// 古いDeploymentを削除
if err := r.Delete(ctx, &deployment); err != nil {
log.Error(err, "failed to delete Deployment resource")
return err
}
log.Info("delete deployment resource: " + deployment.Name)
r.Recorder.Eventf(
nob,
corev1.EventTypeNormal, // import corev1 "k8s.io/api/core/v1"
"Deleted",
"Deleted deployment %q",
deployment.Name,
)
}
return nil
}
- Reconcile 関数を実装します。コントローラー実装の本体をここに記述します。
func (r *NobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("nob", req.NamespacedName)
// Nob Object取得
var nob nobcontrollerv1.Nob
log.Info("fetching Nob resource")
if err := r.Get(ctx, req.NamespacedName, &nob); err != nil {
log.Error(err, "unable to fetch Nob")
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 古いDeploymentが存在したらクリーンナップ
if err := r.cleanupOwnedResources(ctx, log, &nob); err != nil {
log.Error(err, "failed to clean up old Deployment resources for this Nob")
return ctrl.Result{}, err
}
// Deploymentに付与するラベルを作成
labels := map[string]string{
"app": "nginx",
"controller": req.Name,
}
// Deploymentテンプレート作成
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: nob.Spec.DeploymentName,
Namespace: req.Namespace,
Labels: labels,
},
}
// Deploymentを新規作成または更新
if _, err := ctrl.CreateOrUpdate(ctx, r.Client, deploy, func() error {
// nob.specからReplicasをセット
replicas := int32(1)
if nob.Spec.Replicas != nil {
replicas = *nob.Spec.Replicas
}
deploy.Spec.Replicas = &replicas
// spec.selectorにlabelsをセット
if deploy.Spec.Selector == nil {
deploy.Spec.Selector = &metav1.LabelSelector{MatchLabels: labels} // import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
}
// template.objectMetaにlabelsをセット
if deploy.Spec.Template.ObjectMeta.Labels == nil {
deploy.Spec.Template.ObjectMeta.Labels = labels
}
// containersを作成
containers := []corev1.Container{
{
Name: "nginx",
Image: "nginx:latest",
Ports: []corev1.ContainerPort{
{
Name: "nginx-port",
ContainerPort: 80,
},
},
},
}
// template.spec.containersにcontainersをセット
if deploy.Spec.Template.Spec.Containers == nil {
deploy.Spec.Template.Spec.Containers = containers
}
// nobを親とするようリファレンスを設定
if err := ctrl.SetControllerReference(&nob, deploy, r.Scheme); err != nil {
log.Error(err, "unable to set ownerReference from Nob to Deployment")
return err
}
// ctrl.CreateOrUpdate終了
return nil
}); err != nil {
log.Error(err, "unable to ensure deployment is correct")
return ctrl.Result{}, err
}
// Nobが管理するDeploymentを取得
var deployment appsv1.Deployment
var deploymentNamespacedName = client.ObjectKey{
Namespace: req.Namespace,
Name: nob.Spec.DeploymentName,
}
if err := r.Get(ctx, deploymentNamespacedName, &deployment); err != nil {
log.Error(err, "unable to fetch Deployment")
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// NobのStatusが最適化されていれば何もせずreturn
if nob.Status.AvailableReplicas == deployment.Status.AvailableReplicas {
return ctrl.Result{}, nil
}
// NobのStatusを更新
nob.Status.AvailableReplicas = deployment.Status.AvailableReplicas
if err := r.Status().Update(ctx, &nob); err != nil {
log.Error(err, "unable to update Nob status")
return ctrl.Result{}, err
}
// nob.status更新のイベントを登録
r.Recorder.Eventf(
&nob,
corev1.EventTypeNormal,
"Updated",
"Update nob.status.availableReplicas: %d",
nob.Status.AvailableReplicas,
)
return ctrl.Result{}, nil
}
cmd/main.go
Reconciler のメンバを追加します。
if err := (&controller.NobReconciler{ // nob_controller.goの定義に合わせて追記
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("Nob"),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("nob-controller"),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Nob")
os.Exit(1)
}
単体テスト
単体テストを作成および実行します。
テストケース記載
internal/controller/nob_controller_test.go
- BeforeEach に NobSpec で定めたパラメータを定義します。
BeforeEach(func() {
By("creating the custom resource for the Kind Nob")
err := k8sClient.Get(ctx, typeNamespacedName, nob)
if err != nil && errors.IsNotFound(err) {
resource := &nobcontrollerv1.Nob{
ObjectMeta: metav1.ObjectMeta{
Name: resourceName,
Namespace: "default",
},
Spec: nobcontrollerv1.NobSpec{
DeploymentName: "test-deployment",
Replicas: ptr.To[int32](2),
},
}
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
}
})
- AfterEach に Reconciler の定義および、Expect の内容を記載します。
AfterEach(func() {
// TODO(user): Cleanup logic after each test, like removing the resource instance.
resource := &nobcontrollerv1.Nob{}
err := k8sClient.Get(ctx, typeNamespacedName, resource)
Expect(err).NotTo(HaveOccurred())
By("Cleanup the specific resource instance Nob")
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
})
It("should successfully reconcile the resource", func() {
By("Reconciling the created resource")
controllerReconciler := &NobReconciler{ // reconcilerにパラメータを追加
Client: k8sClient,
Log: logf.Log,
Scheme: k8sClient.Scheme(),
Recorder: &record.FakeRecorder{},
}
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
// assertの期待値となるラベル
expectedLabels := map[string]string{
"app": "nginx",
"controller": "test-resource",
}
// 結果のassert
By("Making sure the deployment created successfully")
deployment := &appsv1.Deployment{}
err = k8sClient.Get(
ctx,
client.ObjectKey{
Namespace: "default",
Name: "test-deployment",
},
deployment,
)
Expect(err).NotTo(HaveOccurred())
Expect(deployment.ObjectMeta.Labels).Should(Equal(expectedLabels))
})
テスト実施
下記コマンドでテストを実施します。
make test
ローカルアプリケーション起動
- kind/cluster/kubebuilder-cluster.yaml を下記内容で作成します。
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
- Kind で Kubernetes クラスタを構築します。
kind create cluster --name kubebuilder-cluster --config kind/cluster/kubebuilder-cluster.yaml
- yaml マニフェストの生成および CRD の登録を行います。
make install
- Go プロセスとしてコントローラを動かします。
make run
- config/samples 配下のカスタムリソースの spec を宣言します。
apiVersion: nobcontroller.example.nob/v1
kind: Nob
metadata:
labels:
app.kubernetes.io/name: nob-controller
app.kubernetes.io/managed-by: kustomize
name: nob-sample
spec:
deploymentName: nob-nginx
replicas: 3
- カスタムリソースのマニフェストファイルを apply します。
kubectl apply -f config/samples/nobcontroller_v1_nob.yaml
- 下記コマンドでカスタムリソースが動いていることを確認できます。
$ kubectl get Nob
NAME AGE
nob-sample 15s