LoginSignup
5
3

はじめに

本記事は ラクスパートナーズアドベントカレンダー 2023 2日目 の投稿です。

数年前から話題だったようですが、私は最近プロジェクトで触れる機会があり、遅ればせながら KubernetesOperator に入門したので個人的な備忘もかねて投稿します。
私はバリバリインフラ系のため、Go については完全素人でしたが後述の operator-sdk があったおかげで何とか作り上げることができました。ありがたや。。

そもそも KubernetesOperator とは

ざっくり簡単に言うと
「Kubernetes リソースの作成を自動化してくれるプログラム」です。

CustomResource をアプライすることで Cluster 内に起動した KubernetesOperator がそれを検知し
KubernetesOperator 内で定義してある Deployment や NameSpace 等のリソースを自動でクラスターに起動してくれます。

Operatorhub で様々なプロダクトが開発している KubernetesOperator があるので使用したことがある人も多いと思います。(PrometheusOperator 等。。)

↓ かなりざっくりとした動作イメージ。。
image.png

  1. ユーザーが CustomResource をアプライする。
  2. KubernetesOperator が CustomResource のアプライを検知する。
  3. KubernetesOperator が各リソースをアプライする。

詳しくは RedHat 様の記事 を読んでいただくとより理解していただけると思います。

KubernetesOperator の作成方法はいろいろあるようですが、今回は上記 Redhat 様の記事で紹介されている 「operator-sdk」を使用して KubernetesOperator を作成していきます。

operator-sdk とは

KubernetesOperator の開発、管理を容易にする SDK で、
Go, Ansible, Helm のいずれかを使用した開発に対応しています。

今回はプロジェクトで触れた Go での開発を記載いたします。

※ 筆者は KubernetesOperator で初めて Go に触れましたがそれでも operator-sdk を使うことでかなり簡単に開発をすることができました。

実際に作成してみる

ここから実際に簡単な内容で KubernetesOperator を作成していきます。
淡々と作成していくので都度 operator-sdk のドキュメント を読んで補完していただくとより分かりやすいかもしれません。
(ただの手順書になってしまった。。)

〇 環境情報

プラットフォーム:GCP GKE
KubernetesVersion:1.27.5-gke.200
KubectlVersion:v1.27.5
Go Version:1.19.13
(operator-sdk の前提条件で Go の 1.19.~ が必要です。)

1 operator-sdk のインストール

operator-sdk のドキュメント を確認して環境に合ったインストール方法を選択します。
今回は Git リポジトリからクローンしてインストールします。

  • リポジトリをクローン

    $ git clone https://github.com/operator-framework/operator-sdk
    
  • クローンしたリポジトリに移動する

    $ cd operator-sdk
    
  • make コマンドを使用してインストールする
    少し時間かかります。。

    $ make install
    

2 operator プロジェクトの作成

operator プロジェクトを作成して各種必要な資材を作成します。

  • プロジェクト作成
    --domain--repo は実在するものでなくても問題ありません。

    $ operator-sdk init --domain demo.com --repo github.com/example/demo-operator
    
  • Operator Api の作成
    --kind で指定した文字列が CustomResource の kind になります。

    $ operator-sdk create api --group demo --version v1alpha1 --kind Demo --resource --controller
    

3 CustomResourceDefinition の作成

前手順で作成した資材の中から、CustomResourceDefinition の設定にかかわる部分から修正していきます。

  • ~types.go の修正
    <kind>Spec の構造体に必要な値を追加する

    $ vim api/v1alpha1/demo_types.go
    

    (修正イメージ)
    DemoSpec 等の文字列は前段の create api で指定した --kind によって変わりますので適宜読み替えてください。
    ※ デフォルトのコメントアウトは消しています。

    type DemoSpec struct {
    -   Foo string `json:"foo,omitempty"`
    
    // CustomResource の manifest の値を読み取るために、json:"キー名" を紐づける。
    // omitempty を付けないと必須の値となり、マニフェストで指定していなければアプライ時にエラーが出力される。
    +   Namespace string `json:"namespace"`
    +   Name string `json:"name"`
    +   Image string `json:"image"`
    +   Size int32 `json:"size"`
    }
    
  • CustomResourceDefinition マニフェストの作成

    ~types.go 修正後に以下コマンドを実行すると
    ~types.go の内容に合った CustomResource のアプライに必要なファイルを作成/修正してくれるため修正後は必ず実行します。

    $ make generate
    
    $ make manifests
    

4 Reconcile に処理を追加

KubernetesOperator のメイン処理 (deployment, namespace を作ったり) を記載する部分なので、開発作業の大半はこのファイルをいじることになります。

  • ~controller.go の修正

    $ vi controllers/demo_controller.go
    

    ↓ の Reconcile メソッドにメインの処理を追記します。(もちろん別の関数にしたり、別の pkg から呼び出したりもできますが今回は簡単に Reconcile メソッドのみ修正します。。)

    func (r *DemoReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, 
    error) {
            _ = log.FromContext(ctx)
    
            // TODO(user): your logic here
    
            return ctrl.Result{}, nil
    }
    

    (修正イメージ)

    func (r *DemoReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
            // 要所要所でログを出力したいので logger を定義
            logger := log.FromContext(ctx)
    
            // CustomResourceDefinition がなければ処理を行わない 
            cr := &demov1alpha1.Demo{}
            err := r.Get(ctx, req.NamespacedName, cr)
            if errors.IsNotFound(err) {
                    logger.Info("Demo Resource Not Found")
                    return ctrl.Result{}, nil
            } else if err != nil {
            // 取得自体が失敗したらエラーを出力
                    logger.Error(err, "Failed to Get Demo Resource")
            }
            // それっぽいログを出力
            logger.Info("Create Namespace")
            // Namespace 作成用の構造体を定義
            // 他のリソースを作成したい場合は Go のドキュメントを参照ください。
            ns := &corev1.Namespace{
                    ObjectMeta: metav1.ObjectMeta{
                            Name: cr.Spec.Namespace,
                    },
            }
            // すでに対象リソースがあれば処理を行わない
            err = r.Get(ctx, client.ObjectKey{Name: cr.Spec.Namespace}, ns)
            if errors.IsNotFound(err) {
            // 対象リソースがなければここで作成
                    if err := r.Client.Create(ctx, ns); err != nil {
                            logger.Error(err, "Failed to Create Namespace")
                    }
            } else if err != nil {
                    logger.Error(err, "Failed to Get Namespace")
            }
    
            logger.Info("Namespace creation successfully")
            logger.Info("Create Deployment")
            // Deployment 作成用の構造体を定義
            deploy := &appsv1.Deployment{
                    ObjectMeta: metav1.ObjectMeta{
                            Name:      cr.Spec.Name,
                            Namespace: cr.Spec.Namespace,
                    },
                    Spec: appsv1.DeploymentSpec{
                            Replicas: &cr.Spec.Size,
                            Selector: &metav1.LabelSelector{
                                    MatchLabels: map[string]string{"demo": "true"},
                            },
                            Template: corev1.PodTemplateSpec{
                                    ObjectMeta: metav1.ObjectMeta{
                                            Name:   cr.Spec.Name + "-pod",
                                            Labels: map[string]string{"demo": "true"},
                                    },
                                    Spec: corev1.PodSpec{
                                            Containers: []corev1.Container{
                                                    {
                                                            Name:  cr.Spec.Name + "-container",
                                                            Image: cr.Spec.Image,
                                                    },
                                            },
                                    },
                            },
                    },
            }
            // Namespace と同じく対象リソースがすでにあれば作成しない
            err = r.Get(ctx, client.ObjectKey{Name: cr.Spec.Name, Namespace: cr.Spec.Namespace}, deploy)
            if errors.IsNotFound(err) {
                    if err := r.Client.Create(ctx, deploy); err != nil {
                            logger.Error(err, "Failed to Create Deployment")
                    }
            } else if err != nil {
                    logger.Error(err, "Failed to Get Deployment")
            }
            // 最後にまたそれっぽいログを出力して処理完了
            logger.Info("Deployment creation successfully")
    
            return ctrl.Result{}, nil
    }
    
  • 静的チェック
    (go vet でも同様のチェックができるのでお好きなほうで。。)
     何も出力されなければ OK

    $ make vet
    

5 動作確認

ローカルでオペレーターを起動

  • CustomResourceDefinition のアプライ

    $ make install
    
  • KubernetesOperator のアプライ
    Starting workers と出力されていれば問題なく起動できている。

    $ make run
    

    (出力例)

    ~~ snip ~~
    2023-12-01T13:51:56Z    INFO    controller-runtime.metrics      Metrics server is starting to listen    {"addr": ":8080"}
    2023-12-01T13:51:56Z    INFO    setup   starting manager
    2023-12-01T13:51:56Z    INFO    Starting server {"path": "/metrics", "kind": "metrics", "addr": "[::]:8080"}
    2023-12-01T13:51:56Z    INFO    Starting server {"kind": "health probe", "addr": "[::]:8081"}
    2023-12-01T13:51:56Z    INFO    Starting EventSource    {"controller": "demo", "controllerGroup": "demo.demo.com", "controllerKind": "Demo", "source": "kind source: *v1alpha1.Demo"}
    2023-12-01T13:51:56Z    INFO    Starting Controller     {"controller": "demo", "controllerGroup": "demo.demo.com", "controllerKind": "Demo"}
    2023-12-01T13:51:56Z    INFO    Starting workers        {"controller": "demo", "controllerGroup": "demo.demo.com", "controllerKind": "Demo", "worker count": 1}  
    ~~ snip ~~
    
  • CustomResource のアプライ
    operator-sdk で用意してくれているサンプルマニフェストを少し修正し Operator の CustomResource をアプライする。
    make run を実行しているターミナルとは別のターミナルを開いて行う。

    $ vim config/samples/demo_v1alpha1_demo.yaml
    

    (修正イメージ)
    ~types.go で指定したキーを入れていく。

    apiVersion: demo.demo.com/v1alpha1
    kind: Demo
    metadata:
      labels:
        app.kubernetes.io/name: demo
        app.kubernetes.io/instance: demo-sample
        app.kubernetes.io/part-of: demo-operator
        app.kubernetes.io/managed-by: kustomize
        app.kubernetes.io/created-by: demo-operator
      name: demo-sample
    spec:
      # TODO(user): Add fields here
    +   namespace: demo-namespace
    +   name: demo-deployment
    +   image: nginx
    +   size: 3
    
    $ kubectl apply -f config/samples/demo_v1alpha1_demo.yaml
    

    CustomResource をアプライすると Reconcile メソッドに追加したそれっぽいログが出力される。

    2023-12-01T14:23:49Z    INFO    Create Namespace        {"controller": "demo", "controllerGroup": "demo.demo.com", "controllerKind": "Demo", "Demo": {"name":"demo-sample","namespace":"default"}, "namespace": "default", "name": "demo-sample", "reconcileID": "9ae44549-3b16-4087-bd42-e1d5feaae1e2"}
    2023-12-01T14:23:50Z    INFO    Namespace creation successfully {"controller": "demo", "controllerGroup": "demo.demo.com", "controllerKind": "Demo", "Demo": {"name":"demo-sample","namespace":"default"}, "namespace": "default", "name": "demo-sample", "reconcileID": "9ae44549-3b16-4087-bd42-e1d5feaae1e2"}
    2023-12-01T14:23:50Z    INFO    Create Deployment       {"controller": "demo", "controllerGroup": "demo.demo.com", "controllerKind": "Demo", "Demo": {"name":"demo-sample","namespace":"default"}, "namespace": "default", "name": "demo-sample", "reconcileID": "9ae44549-3b16-4087-bd42-e1d5feaae1e2"}
    2023-12-01T14:23:50Z    INFO    Deployment creation successfully        {"controller": "demo", "controllerGroup": "demo.demo.com", "controllerKind": "Demo", "Demo": {"name":"demo-sample","namespace":"default"}, "namespace": "default", "name": "demo-sample", "reconcileID": "9ae44549-3b16-4087-bd42-e1d5feaae1e2"}
    

    さらに定義した Namespace と Deployment も問題なくアプライされている。

    $ kubectl get ns
    NAME              STATUS   AGE
    default           Active   2d10h
    demo              Active   24h
    demo-namespace    Active   2m5s
    gmp-public        Active   2d10h
    gmp-system        Active   2d10h
    kube-node-lease   Active   2d10h
    kube-public       Active   2d10h
    kube-system       Active   2d10h
    
    $ kubectl get all -n demo
    NAME                       READY   STATUS    RESTARTS   AGE
    pod/demo-c9fd6db65-kxg64   1/1     Running   0          23h
    pod/demo-c9fd6db65-vlhl5   1/1     Running   0          23h
    pod/demo-c9fd6db65-z6scj   1/1     Running   0          23h
    
    NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
    deployment.apps/demo   3/3     3            3           24h
    
    NAME                             DESIRED   CURRENT   READY   AGE
    replicaset.apps/demo-c9fd6db65   3         3         3       24h
    
  • 動作確認まで完了したので最後にイメージを作成してクラスター内で起動する。

    イメージをビルドする前にもうひと手間加えます。
    Reconciler 構造体の下にあるこの部分。

    //+kubebuilder:rbac:groups=demo.demo.com,resources=demoes,verbs=get;list;watch;create;update;patch;delete 
    //+kubebuilder:rbac:groups=demo.demo.com,resources=demoes/status,verbs=get;update;patch
    //+kubebuilder:rbac:groups=demo.demo.com,resources=demoes/finalizers,verbs=update
    

    初見時はぱっと見コメントだと思いましたが、ここに rbac の設定を記載しておけばビルド時に rbac のリソースをよしなに作ってくれます。便利!
    今回作成したいのは Namespace と Deployment なのでこんな感じで追記。

     //+kubebuilder:rbac:groups=demo.demo.com,resources=demoes,verbs=get;list;watch;create;update;patch;delete
    //+kubebuilder:rbac:groups=demo.demo.com,resources=demoes/status,verbs=get;update;patch
    //+kubebuilder:rbac:groups=demo.demo.com,resources=demoes/finalizers,verbs=update
    //+kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch;create;update;patch
    //+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch
    

    追記したら make コマンドを使ってイメージをビルドする。

    $ make docker-build
    

    ビルドしたら適当なレジストリにプッシュする。
    その後以下のコマンドを実行して、オペレーターをクラスター内に起動する。

    $ make deploy IMG=<IMAGEPATH>:<TAG>
    
    $ kubectl get ns
    NAME                   STATUS   AGE
    default                Active   2d12h
    ↓ ここにオペレーターのリソースが入っている。
    demo-operator-system   Active   22m
    gmp-public             Active   2d12h
    gmp-system             Active   2d12h
    kube-node-lease        Active   2d12h
    kube-public            Active   2d12h
    kube-system            Active   2d12h
    $ kubectl get all -n demo-operator-system
    NAME                                                    READY   STATUS    RESTARTS   AGE
    pod/demo-operator-controller-manager-68fc6f6cc8-v6znr   2/2     Running   0          23m
    
    NAME                                                       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
    service/demo-operator-controller-manager-metrics-service   ClusterIP   10.60.4.79   <none>        8443/TCP   23m
    
    NAME                                               READY   UP-TO-DATE   AVAILABLE   AGE
    deployment.apps/demo-operator-controller-manager   1/1     1            1           23m
    
    NAME                                                          DESIRED   CURRENT   READY   AGE
    replicaset.apps/demo-operator-controller-manager-68fc6f6cc8   1         1         1       23m
    

    再度 CustomResource をアプライする。

    $ kubectl apply -f config/samples/demo_v1alpha1_demo.yaml
    
    $ kubectl get ns
    NAME                   STATUS   AGE
    default                Active   2d13h
    demo-namespace         Active   51s
    demo-operator-system   Active   60s
    gmp-public             Active   2d13h
    gmp-system             Active   2d13h
    kube-node-lease        Active   2d13h
    kube-public            Active   2d13h
    kube-system            Active   2d13h
    $ kubectl get all -n demo-namespace
    NAME                                   READY   STATUS    RESTARTS   AGE
    pod/demo-deployment-6db7ddf7d9-7px8s   1/1     Running   0          62s
    pod/demo-deployment-6db7ddf7d9-7v8kx   1/1     Running   0          62s
    pod/demo-deployment-6db7ddf7d9-xrd2s   1/1     Running   0          62s
    
    NAME                              READY   UP-TO-DATE   AVAILABLE   AGE
    deployment.apps/demo-deployment   3/3     3            3           62s
    
    NAME                                         DESIRED   CURRENT   READY   AGE
    replicaset.apps/demo-deployment-6db7ddf7d9   3         3         3       62s
    

    問題なく作成できているのでヨシ!!

以上!!

簡潔な内容ですが以上です。

正直 KubernetesOperator が刺さる要件ってそこまで多くない気はしていますが
私がまだまだ Go と仲良くなれていないため、KubernetesOperator の良さを引き出せていない。というのもあるので今後も修行し続ければもっと良い使い方が見つかるかも。。

とりあえず Kubernetes を頻繁に触っている方や、"自動化" が大好物な方はかなり楽しいので触ってみることをお勧めします。

本当は CustomResource の削除時に自動で削除とか、エコシステムのクライアント使ってリソース作成とかいろいろ書こうと思っていましたが長くなってしまうのでまたそのうち書きます。。

参考文献

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3