虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

KubebuilderでKubernetes カスタムリソースを実装する

f:id:toranoana-lab:20211209131826p:plain

※本記事は予約投稿です

こんにちは、虎の穴ラボのY.N.です。

虎の穴ラボ Advent Calendar 2021 - Qiita 11日目の記事です。

10日目はH.Kさんによる KotlinでRuby/Railsっぽい便利なプロパティ(メソッド)を使えるようにしてみる! でした。

12日目はA.M.さんが リモートワークでの働き方 という記事を書く予定です。

はじめに

Kubernetesを使っていると、cert-managerやIstioなどの便利なリソースを使うことが多いと思います。
しかし、これらはKubernetesの標準装備というわけではなく、カスタムリソース(以下、CR)と呼ばれ、名前の通りカスタマイズされたリソースなのです。
もちろん、「カスタムリソース」と呼ばれているくらいなので、任意の動きをするリソースを自作することもでき、
ご自身のKubernetesクラスタに乗せて使用することができます。
そこで、この記事では、KubernetesのCRを実装してみようということで、
KubebuilderというツールのTutorialを実践していく記事になります。


目次

前提知識

本記事は、Kubernetesのコアな部分について実践する記事となっているため、
下記の知識があるとより理解が深まるような内容となっております。

  • Kubernetes関連知識
    • Kubernetesマニフェストの書き方
    • Kubernetesの標準リソースについての知識
    • Controller
    • RBAC
    • Admission Webhook
  • Go言語の基礎文法

実行環境

本記事はMacPCで行うことを前提としています。
私が実際に動作を確認した環境は下記のとおりです。

  • macOS Catalina 10.15
  • bash 5.1.12
  • go 1.17.3
  • make 3.81
  • docker 20.10.8
  • kubernetes v1.22
  • kubebuilder 3.2.0
  • kind v0.11.1 go 1.16.4
  • kubectl client 1.21, server 1.16
  • kustomize v4.0.4

必要なもの

Kubebuilder Tutorial の実施には以下の物が必要となります。
本記事では、これらの環境構築については記載していないため、必要に応じて準備していただけますと幸いです。

  • Go言語実行環境
  • Docker
  • kind
  • kubectl
  • kustomize
  • make

Kubebuilderとは

github.com

Kubebuilderは、カスタムリソース定義(以下、CRD)を使用してKubernetesAPIを構築するためのフレームワークです。(README.mdより抜粋)
Kubebuilderを使用することで、CRを実装するのに必要なテンプレートが用意されます。
また、KubebuilderはCRの実装に注力できるように、マニフェストの作成やデプロイなどはMakefileを使用しての簡略化が実現されているなど、実装初心者でも使いやすいツールです。

環境構築

KubebuilderのQuick Startに従ってKubebuilderのバイナリを準備する必要があります。

$ curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)
$ chmod +x kubebuilder && mv kubebuilder /usr/local/bin/

バイナリを取得できたら実行できるかを確認してください。

$ kubebuilder version

こんな感じで出てきたらOKです。

Version: main.version{KubeBuilderVersion:"3.2.0", KubernetesVendor:"1.22.1", GitCommit:"b7a730c84495122a14a0faff95e9e9615fffbfc5", BuildDate:"2021-10-29T18:32:16Z", GoOs:"darwin", GoArch:"amd64"}

実践 kubebuilder tutorial

今回実施していくのは、Kubebuilder Tutorialの 1. Tutorial: Building Cronjobです。
このTutorialでは、CronJobのCRを作成し、テストまでを行います。

book.kubebuilder.io

作業ディレクトリの準備(1. Tutorial: Building Cronjob)

このページでは、作業ディレクトリ(プロジェクト)の準備を行います。

$ mkdir project
$ cd project
$ kubebuilder init --domain tutorial.kubebuilder.io --repo tutorial.kubebuilder.io/project

このコマンドを実行すると、projectディレクトリができ、テンプレートファイル群が出来上がります。

main.goのコーディング(1.2 Every journey needs a start, every program a main)

ここで、projectディレクトリ内に作成されたmain.goのコーディングを行います。
main.goはCRのコントローラやWebhookを実行するマネージャとして機能します。

完成するソースコードは下記のようになります。

ソースコードを見ると、気になるコメントがありますが、これはマーカーコメントと呼ばれるコメントです。
kubebuilderはこのマーカーコメントを解析してkubernetes マニフェストを作成するので、これらのコメントは内容に従って必ず記載してください。

//+kubebuilder:scaffold:imports

APIの作成(1.4. Adding a new API)

さて、次にCronJobのAPIを作成します
次のコマンドでapiのテンプレートを作成します。

$ kubebuilder create api --group batch --version v1 --kind CronJob

Create Resouce と Create Controllerと聞かれるので y で先に進みます。
すると、ディレクトリ内にapiディレクトリとcontrollersディレクトリが作成されます。
apiディレクトリの中にはv1ディレクトリがあり、cronjob_types.goというファイルが出来上がっていると思います。
また、controllersディレクトリの中にはcronjob_controller.goというファイルが出来上がっていると思います。
これらのファイルについてもmain.goと同様に次以降の章で実装を行っていきます。

構造体の実装 (1.5. Designing an API)

1.5. Designing an API ではcronjob_types.goのコーディングを行っていきます。
最終的なコードは下記のとおりです。

cronjob_types.goはテンプレートとして、CronJobSpecとCronJobStatus、CronJob、CronJobListという構造体を持っています。
CronJobSpecにはKubernetes マニフェストに記述するspecの内容について記載しています。
下記のようにマーカーコメントでvalidationやoptionalの設定を行うことができます。
この場合、Kubernetes マニフェストに記述するspec.startingDeadlineSecondsは、最低値が0でオプションとするコードになります。

//+kubebuilder:validation:Minimum=0
// +optional
StartingDeadlineSeconds *int64 `json:"startingDeadlineSeconds,omitempty"`

CronJobStatusはkubectl describe等でリソースの状態を見ようとした際に確認できる内容を記載します。
基本的な書き方はCronJobSpecと同様です。

CronJobはルートオブジェクトになります。
ここの記載は基本的にテンプレート通りで問題ありません。
SpecにCronJobSpecを、StatusにCronJobStatusを使用するように書かれています。

Controllerの実装(1.6. What's in a controller? ~ 1.7. Implementing a controller)

kubebuilderによって作成されたcontrollersディレクトリの中身を見てみると、cronjob_controller.goがあります。
これがCronJobのコントローラ、メインロジックになります。

Controllerテンプレートには、あらかじめReconciler構造体とReconcile関数、SetUpWithManager関数が定義されています。

最終的なコードは下記のとおりです。

CronJobReconciler構造体は下記のように実装します。

type CronJobReconciler struct {
    client.Client
    Scheme *runtime.Scheme
    Clock
}

Controllerが関連するオブジェクトを操作するため、いくつかのRBAC権限が必要です。
RBAC用のマーカーコメントを記述することで実行に必要なRBAC権限を記載します。
下記のように記載することで、Controllerが、resourcesで指定されたリソースに対して、verbsで指定した操作を行うことができる権限を与えることができます。

//+kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/finalizers,verbs=update
//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=batch,resources=jobs/status,verbs=get

そして、次はメインロジックとなるReconcile関数の実装です。(1.7. Implementing a controller)

1: Load the CronJob by name

r.Getで、req.NamespacedNameで指定されたNamespaceとNameを持つCronJobオブジェクトを取得します。
オブジェクトが取得できなかった場合はclient.IgnoreNotFound(err)をreturnすることでNotFoundエラーをスキップしています。
これをスキップしないとRequeueして再度Reconcileを実行した場合、NotFoundエラーが出続けてしまうためです。

var cronJob batchv1.CronJob
if err := r.Get(ctx, req.NamespacedName, &cronJob); err != nil {
    log.Error(err, "unable to fetch CronJob")
    return ctrl.Result{}, client.IgnoreNotFound(err)
}

2: List all active jobs, and update the status

r.ListでJobのリストを取得します。

var childJobs kbatch.JobList
if err := r.List(ctx, &childJobs, client.InNamespace(req.Namespace), client.MatchingFields{jobOwnerKey: req.Name}); err != nil {
    log.Error(err, "unable to list child Jobs")
    return ctrl.Result{}, err
}

Jobのリストを取得したら、状態を確認し、Active、Failed、Successfulに振り分けを行います。
振り分けたJobのリストは後のロジックで使用されます。

for i, job := range childJobs.Items {
    _, finishedType := isJobFinished(&job)
    switch finishedType {
    case "":
        activeJobs = append(activeJobs, &childJobs.Items[i])
    case kbatch.JobFailed:
        failedJobs = append(failedJobs, &childJobs.Items[i])
    case kbatch.JobComplete:
        successfulJobs = append(successfulJobs, &childJobs.Items[i])
    }
    ~~~
}

このロジックの最後で、activeなCronJobのStatusを更新しています。

cronJob.Status.Active = nil
for _, activeJob := range activeJobs {
    jobRef, err := ref.GetReference(r.Scheme, activeJob)
    if err != nil {
        log.Error(err, "unable to make reference to active job", "job", activeJob)
        continue
    }
    cronJob.Status.Active = append(cronJob.Status.Active, *jobRef)
}
if err := r.Status().Update(ctx, &cronJob); err != nil {
    log.Error(err, "unable to update CronJob status")
    return ctrl.Result{}, err
}

3: Clean up old jobs according to the history limit

終了しているジョブ(failedJobs、successfulJobs)について、履歴上限を超えたものを削除します。

if cronJob.Spec.FailedJobsHistoryLimit != nil {
    sort.Slice(failedJobs, func(i, j int) bool {
        if failedJobs[i].Status.StartTime == nil {
            return failedJobs[j].Status.StartTime != nil
        }
        return failedJobs[i].Status.StartTime.Before(failedJobs[j].Status.StartTime)
    })
    for i, job := range failedJobs {
        if int32(i) >= int32(len(failedJobs))-*cronJob.Spec.FailedJobsHistoryLimit {
            break
        }
        if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
            log.Error(err, "unable to delete old failed job", "job", job)
        } else {
            log.V(0).Info("deleted old failed job", "job", job)
        }
    }
}

if cronJob.Spec.SuccessfulJobsHistoryLimit != nil {
    sort.Slice(successfulJobs, func(i, j int) bool {
        if successfulJobs[i].Status.StartTime != nil {
            return successfulJobs[j].Status.StartTime != nil
        }
        return successfulJobs[i].Status.StartTime.Before(successfulJobs[j].Status.StartTime)
    })
    for i, job := range successfulJobs {
        if int32(i) >= int32(len(successfulJobs))-*cronJob.Spec.SuccessfulJobsHistoryLimit {
            break
        }
        if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); (err) != nil {
            log.Error(err, "unable to delete old sucessful job", "job", job)
        } else {
            log.V(0).Info("deleted old successful job", "job", job)
        }
    }
}

4: Check if we’re suspended

オブジェクトが一時停止している場合、ジョブを実行せず停止します。

if cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend {
    log.V(1).Info("cronjob suspended, skipping")
    return ctrl.Result{}, nil
}

5: Get the next scheduled run

次にスケジュールされているJobを取得します。

missedRun, nextRun, err := getNextSchedule(&cronJob, r.Now())
if err != nil {
    log.Error(err, "unable to figure out CronJobschedule")
    return ctrl.Result{}, nil
}

6: Run a new job if it’s on schedule, not past the deadline, and not blocked by our concurrency policy

ジョブが実行できる状態であれば実際にジョブを実行します。

// 実行されていないジョブがあるか
if missedRun.IsZero() {
    log.V(1).Info("no upcoming scheduled times, sleeping until next")
    return scheduledResult, nil
}

// 予定開始時刻のデッドラインを過ぎていないか
log = log.WithValues("current run", missedRun)
tooLate := false
if cronJob.Spec.StartingDeadlineSeconds != nil {
    tooLate = missedRun.Add(time.Duration(*cronJob.Spec.StartingDeadlineSeconds) * time.Second).Before(r.Now())
}
if tooLate {
    log.V(1).Info("missed starting deadline for last run, sleeping till next")
    return scheduledResult, nil
}

// 同時実行ポリシーがForbidConcurrent(同時実行禁止)でactiveなJobがあるか
if cronJob.Spec.ConcurrencyPolicy == batchv1.ForbidConcurrent && len(activeJobs) > 0 {
    log.V(1).Info("concurrency policy blocks concurrent runs, skipping", "num active", len(activeJobs))
    return scheduledResult, nil
}

// 同時実行ポリシーでReplaceCOncurrent(置き換え)が指定されている場合はactiveなJobを置き換えるために削除する
if cronJob.Spec.ConcurrencyPolicy == batchv1.ReplaceConcurrent {
    for _, activeJob := range activeJobs {
        if err := r.Delete(ctx, activeJob, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
            log.Error(err, "unable to delete active job", "job", activeJob)
            return ctrl.Result{}, err
        }
    }
}

7: Requeue when we either see a running job or it’s time for the next scheduled run

最後に、結果をreturnします。

// scheduledResult := ctrl.Result{RequeueAfter: nextRun.Sub(r.Now())}
return scheduledResult, nil

SetupWithManagerの実装

SetupWithManagerではControllerが監視するリソースの種別をctrl.NewControllerManagedByで指定しています。
ForにはControllerが管理するCRを指定するため、ここではCronJobを指定します。
OwnsにはForで指定したリソース(CronJob)がkbatch.Jobのリソースを作成するため、kbatch.Jobを指定します。
これによってCronJobまたはCronJob Controllerが作成したkbatch.JobのEventを察知してReconcile処理が呼び出されるようになります。

func (r *CronJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
    ...
    return ctrl.NewControllerManagedBy(mgr).
        For(&batchv1.CronJob{}).
        Owns(&kbatch.Job{}).
        Complete(r)
}

Webhookの実装(1.8. Implementing defaulting/validating webhooks)

次は、Admission Webhookを実装する場合の内容になります。
まずは、kubebuilderの機能で Admission Webhookのテンプレートを作成します。
--defaulting オプションで Defaulting Webhook用のコード、
--programmatic-validation オプションで Validating Webhook用のコードを自動生成してくれます。

$ kubebuilder create webhook --group batch --version v1 --kind CronJob --defaulting --programmatic-validation

これを実行すると、api/v1ディレクトリの直下にcronjob_webhook.goが作成されます。
本来、SetupWebhookWithManagerの呼び出しがmain.goに追記されますが、本記事ではすでに完成形のコードを記載しているため、変化がないように見えている点だけ注意してください。
また、テスト時にWebhookを無効化して実行するため、環境変数で無効化できるようにしています。

if os.Getenv("ENABLE_WEBHOOKS") != "false" {
    if err = (&batchv1.CronJob{}).SetupWebhookWithManager(mgr); err != nil {
        setupLog.Error(err, "unable to create webhook", "webhook", "CronJob")
        os.Exit(1)
    }
}

cronjob_webhook.goの最終的なコードは下記のとおりです。

Webhookテンプレートには、SetupWebhookWithManager、Default、ValidateCreate、ValidateUpdate、ValidateDeleteが予め定義されています。
SetupWebhookWithManagerはこのままで大丈夫ですが、他の4つについては実装する必要があります。

Default関数(Mutating Admission Webhook)の実装

Default関数はSpecに値が設定されていないものについて、デフォルト値を設定するようにします。

func (r *CronJob) Default() {
    cronjoblog.Info("default", "name", r.Name)

    // TODO(user): fill in your defaulting logic.

    if r.Spec.ConcurrencyPolicy == "" {
        r.Spec.ConcurrencyPolicy = AllowConcurrent
    }
    if r.Spec.Suspend == nil {
        r.Spec.Suspend = new(bool)
    }
    if r.Spec.SuccessfulJobsHistoryLimit == nil {
        r.Spec.SuccessfulJobsHistoryLimit = new(int32)
        *r.Spec.SuccessfulJobsHistoryLimit = 3
    }
    if r.Spec.FailedJobsHistoryLimit == nil {
        r.Spec.FailedJobsHistoryLimit = new(int32)
        *r.Spec.FailedJobsHistoryLimit = 1
    }
}

ValidateXXX関数(Validating Admission Webhook)の実装

Kubebuilder TutorialのValidateCreate, ValidateUpdate関数は、リソースのcreate、update時に同じvalidationが効くようにするため、validateCronJobという関数を作成し、それを呼び出すようにしています。

func (r *CronJob) ValidateCreate() error {
    cronjoblog.Info("validate create", "name", r.Name)

    return r.validateCronJob()
}

func (r *CronJob) ValidateUpdate(old runtime.Object) error {
    cronjoblog.Info("validate update", "name", r.Name)

    return r.validateCronJob()
}

ValidateDelete関数は今回削除時のvalidationが不要なため何もしないようにしています。

func (r *CronJob) ValidateDelete() error {
    cronjoblog.Info("validate delete", "name", r.Name)

    return nil
}

validateCronJobではnameとspec.scheduleの内容をvalidationするロジックを記述しています。

マニフェストの作成 〜 実行(1.9. Running and deploying the controller)

マニフェストの作成

kubebuilderではmakefileが用意されており、makeコマンドを使用することでコードの内容からCR、CRDのmanifestを作成してくれます。

$ make manifests

作成したmanifestをclusterに適用します
※適用にはkubectl applyを使用しているので、kubectl config current-contextでどのclusterを使用しているか確認してください。
本記事ではkindで作成したclusterを使用しています。

$ make install

make installの結果が下記のようになっていれば、CronJobのCRDが作成されているので成功となります。

/Users/xxxx/workspace/kubebuilder/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/Users/xxxx/workspace/kubebuilder/bin/kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/cronjobs.batch.tutorial.kubebuilder.io created

kubectl get crdsで確認してみると、作成したCRDが適用されているのがわかります。

$ kubectl get crds | grep cronjob
cronjobs.batch.tutorial.kubebuilder.io       2021-12-08T04:28:05Z

CRDが適用できたのでいよいよ実行していきます。
この際ローカルでのテストになるためWebhookは無効化して実行します。

$ make run ENABLE_WEBHOOKS=false
...
2021-12-08T13:40:11.294+0900    INFO    controller-runtime.metrics      metrics server is starting to listen      {"addr": ":8080"}
2021-12-08T13:40:11.295+0900    INFO    setup   starting manager
2021-12-08T13:40:11.396+0900    INFO    starting metrics server {"path": "/metrics"}
2021-12-08T13:40:11.396+0900    INFO    controller.cronjob      Starting EventSource      {"reconciler group": "batch.tutorial.kubebuilder.io", "reconciler kind": "CronJob", "source": "kind source: /, Kind="}
2021-12-08T13:40:11.396+0900    INFO    controller.cronjob      Starting EventSource      {"reconciler group": "batch.tutorial.kubebuilder.io", "reconciler kind": "CronJob", "source": "kind source: /, Kind="}
2021-12-08T13:40:11.396+0900    INFO    controller.cronjob      Starting Controller       {"reconciler group": "batch.tutorial.kubebuilder.io", "reconciler kind": "CronJob"}
2021-12-08T13:40:11.497+0900    INFO    controller.cronjob      Starting workers {"reconciler group": "batch.tutorial.kubebuilder.io", "reconciler kind": "CronJob", "worker count": 1}

コントローラが起動しました。
次に、CronJobリソースを作成します。config/samples/batch_v1_cronjob.yamlを編集しましょう。

apiVersion: batch.tutorial.kubebuilder.io/v1
kind: CronJob
metadata:
  name: cronjob-sample
spec:
  # TODO(user): Add fields here
  schedule: "*/1 * * * *"
  startingDeadlineSeconds: 60
  concurrencyPolicy: Allow
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            args:
            - /bin/sh
            - -c
            - date; echo Hello from the kubernetes cluster
          restartPolicy: OnFailure

これは、1分ごとに日付と「Hello from the kubernetes cluster」をechoするだけのCronJobです。
新しくターミナルを開き、これを適用します。

$ kubectl apply -f config/samples/batch_v1_cronjob.yaml
cronjob.batch.tutorial.kubebuilder.io/cronjob-sample created

リソースが作成されているか確認してみましょう。
CronJobリソースの確認

$ kubectl get cronjob.batch.tutorial.kubebuilder.io
NAME             AGE
cronjob-sample   33s

$ kubectl get cronjob.batch.tutorial.kubebuilder.io -o yaml

apiVersion: v1
items:
- apiVersion: batch.tutorial.kubebuilder.io/v1
  kind: CronJob
  metadata:
    annotations:
      kubectl.kubernetes.io/last-applied-configuration: |
        {"apiVersion":"batch.tutorial.kubebuilder.io/v1","kind":"CronJob","metadata":{"annotations":{},"name":"cronjob-sample","namespace":"default"},"spec":{"concurrencyPolicy":"Allow","jobTemplate":{"spec":{"template":{"spec":{"containers":[{"args":["/bin/sh","-c","date; echo Hello from the kubernetes cluster"],"image":"busybox","name":"hello"}],"restartPolicy":"OnFailure"}}}},"schedule":"*/1 * * * *","startingDeadlineSeconds":60}}
    creationTimestamp: "2021-12-08T04:47:18Z"
    generation: 1
    managedFields:
    - apiVersion: batch.tutorial.kubebuilder.io/v1
      fieldsType: FieldsV1
      fieldsV1:
        f:metadata:
          f:annotations:
            .: {}
            f:kubectl.kubernetes.io/last-applied-configuration: {}
        f:spec:
          .: {}
          f:concurrencyPolicy: {}
          f:jobTemplate:
            .: {}
            f:spec:
              .: {}
              f:template:
                .: {}
                f:spec:
                  .: {}
                  f:restartPolicy: {}
          f:schedule: {}
          f:startingDeadlineSeconds: {}
      manager: kubectl-client-side-apply
      operation: Update
      time: "2021-12-08T04:47:18Z"
    - apiVersion: batch.tutorial.kubebuilder.io/v1
      fieldsType: FieldsV1
      fieldsV1:
        f:spec:
          f:jobTemplate:
            f:metadata: {}
            f:spec:
              f:template:
                f:metadata: {}
                f:spec:
                  f:containers: {}
        f:status: {}
      manager: main
      operation: Update
      time: "2021-12-08T04:47:18Z"
    name: cronjob-sample
    namespace: default
    resourceVersion: "21295136"
    selfLink: /apis/batch.tutorial.kubebuilder.io/v1/namespaces/default/cronjobs/cronjob-sample
    uid: 6363c17a-9d46-4c3d-8624-0dda1a071195
  spec:
    concurrencyPolicy: Allow
    jobTemplate:
      spec:
        template:
          spec:
            containers:
            - args:
              - /bin/sh
              - -c
              - date; echo Hello from the kubernetes cluster
              image: busybox
              name: hello
            restartPolicy: OnFailure
    schedule: '*/1 * * * *'
    startingDeadlineSeconds: 60
  status: {}
kind: List
metadata:
  resourceVersion: ""
  selfLink: ""

Jobリソースの確認

$ kubectl get job
NAME                        COMPLETIONS   DURATION   AGE
cronjob-sample-1638989280   1/1           8s         3m7s
cronjob-sample-1638989340   1/1           4s         2m7s
cronjob-sample-1638989400   1/1           4s         67s
cronjob-sample-1638989460   1/1           3s         7s

動作確認が取れたのでCtrl + Cでmake runをしているプロセスを停止、
適用済みのCronJobリソースを削除します。

$ kubectl delete -f config/samples/batch_v1_cronjob.yaml

イメージの作成(1.9. Running and deploying the controller)

DockerHubに作成したControllerのイメージをデプロイします。 ※<some-registry>, <project-name>, tagは適宜ご自身の環境に合わせて書き換えてください。

$ make docker-build docker-push IMG=<some-registry>/<project-name>:tag
$ make deploy IMG=<some-registry>/<project-name>:tag

cert-manager, webhookのデプロイ(1.9.1. Deploying the cert manager ~ 1.9.2. Deploying webhooks)

テストにあたって、webhook serverの証明書発行に使用しているcert-managerをclusterにインストールする必要があります。

$ kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.6.0/cert-manager.yaml

cert-managerのインストールができているか確認します。
下記のように出ていればインストール成功です。

$ kubectl get pods -n cert-manager
NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-77fd97f598-khfwn              1/1     Running   0          91s
cert-manager-cainjector-7974c84449-zmh9n   1/1     Running   0          91s
cert-manager-webhook-5f4b965fbd-6bqlg      1/1     Running   0          91s

続けて、マニフェスト内のcert-managerとwebhookを適用する部分がデフォルトではコメントアウトされているので、このコメントを外します。

これをデプロイします。

$ make deploy IMG=<some-registry>/<project-name>:tag

証明書が発行されるまで待ちます(おおよそ1分程度)
下記のように出たら証明書が発行されています

$ kubectl get certificate -A
NAMESPACE         NAME                    READY   SECRET                AGE
projects-system   projects-serving-cert   True    webhook-server-cert   16m

再度、CronJobを作成して動作を確認するとmake runを実行していない状態でもJobが動いているのを確認できます。

$ kubectl apply -f config/samples/batch_v1_cronjob.yaml
cronjob.batch.tutorial.kubebuilder.io/cronjob-sample created
$ kubectl get cronjob.batch.tutorial.kubebuilder.io
NAME             AGE
cronjob-sample   7m22s
FVFD40TYP3YV:kubebuilder 1080$ kubectl get job
NAME                        COMPLETIONS   DURATION   AGE
cronjob-sample-1638993360   1/1           3s         2m6s
cronjob-sample-1638993420   1/1           4s         69s
cronjob-sample-1638993480   1/1           3s         12s

テスト(1.10. Writing tests)

最後に、カスタムコントローラのテストについて実施していきます。
controllersディレクトリ内に suite_test.goというファイルが作成されています。
これは、main.goと同じような流れでテスト用コントローラのセットアップを行うテストプログラムです。
そのため、これだけだとカスタムコントローラのテストはできないので、<カスタムコントローラ名>_test.goというファイルを作成する必要があります。

実際のコードは下記のようになります。

テストの実施は下記のコマンドを controllersディレクトリで実行します。

$ go test ./...

が、これではなんと動きません。。。
Tutorialには記載されていませんが、go testでテストを行う場合、ローカル環境に下記のバイナリが必要になります。

  • kube-apiserver
  • kubectl
  • etcd

kubernetesのビルド

というわけで、kube-apiserver、kubectlのバイナリを作成するため、kubernetesのビルドを行っていきます。
kubernetesはREADME.mdに従ってビルドを行えば問題なく完了できます。
ビルドにはそれなりに時間がかかります。
また、kubernetesのビルドにはversion 4.2以上のbashが必要ですので注意してください。

$ export GOPATH=$(go env GOPATH)
$ mkdir -p $GOPATH/src/k8s.io
$ cd $GOPATH/src/k8s.io
$ git clone https://github.com/kubernetes/kubernetes
$ cd kubernetes
$ bash ※zsh等のシェルを使っている方のみ
$ make

完了すると _output/binにビルド成果物が出来上がります。
下記の手順に従ってファイルをコピーしてください。

$ sudo mkdir -p /usr/local/kubebuilder/bin
$ sudo cp _output/bin/kube-apiserver /usr/local/kubebuilder/bin
$ sudo cp _output/bin/kubectl /usr/local/kubebuilder/bin

etcdのビルド

etcdのバイナリについてもetcdの公式ページに従ってビルドしていきます。

$ git clone https://github.com/coreos/etcd.git
$ cd etcd
$ ./build

完了すると /binにビルド成果物が出来上がります。
下記の手順に従ってファイルをコピーしてください。

$ sudo cp bin/etcd /usr/local/kubebuilder/bin

さて、改めてテストを実行します。

$ go test ./...
ok      tutorial.kubebuilder.io/project/controllers     21.423s

OKが出たのでテストが通っていることが確認できました。

おわりに

さて、ここまでkubebuilder tutorialを実践してきました。
CRの実装は非常に複雑で、概念も難しいものが多かったですが、なんとかこうして実装することができました。
リソースがどう制御されているのか、Reconcilerがどうリソースに働きかけるのか、そんなところが少しでもこの記事を読んで理解できていたなら幸いです。
非常に長い記事になってしまいましたが、ここまで読んでいただきありがとうございました。

書籍の紹介

最後に、KubernetesのCRを作成するために参考させていただきました書籍を紹介させていただきます。
実践入門 Kubernetes カスタムコントローラーへの道

です。
本記事はKubebuilder Tutorialの内容について記載していますが、この書籍の中でもkubebuilderを使用したカスタムコントローラの実装について細かく解説をしてくれています。
また、Kubernetes公式が出しているカスタムコントローラー実装練習用のリポジトリ「sample-controller」の内容をなぞりながらカスタムコントローラ実装についてわかりやすく書かれている本だと思いますので、ぜひおすすめの本です。

P.S.

採用情報
■募集職種
yumenosora.co.jp

カジュアル面談も随時開催中です
■お申し込みはこちら!
news.toranoana.jp

■ToraLab.fmスタートしました!
メンバーによるPodcastを配信中!
是非スキマ時間に聞いて頂けると嬉しいです。
anchor.fm
■Twitterもフォローしてくださいね!
ツイッターでも随時情報発信をしています
twitter.com