Kubernetes Operator初心者がOperatorを作成してみた (operator-sdk編)

# 目的

Kubernetes Operatorは初心者にとってはとっつきにくいと思うので、自分の備忘録を残しておく。今回は、operator-sdkのTutorialを利用してOperatorへの理解を深める

# 内容

[Go Operator Tutorial (operator-sdk)](https://sdk.operatorframework.io/docs/building-operators/golang/tutorial/)のmemcached-operatorを実装する. ([memcached_controller.go](https://github.com/operator-framework/operator-sdk/blob/v1.3.0/testdata/go/v3/memcached-operator/controllers/memcached_controller.go))

memcached-operatorの内容:

- `Memcached` というCustom Resource
    - `spec.size`でMemcachedのNodeの数を指定できる
    - `status.nodes`でNode情報をもつ
- ControllerでReconcileLoopを実装
    - Memcachedインスタンスを取得
    - Deploymentがなければ作成
    - MemcachedのsizeでとDeploymentのreplicas数を同じにする
    - status.nodesにPod名を格納する

# Prerequisite

- [Git](https://git-scm.com/)
- [Go](https://golang.org/) version 1.15
- [Docker](https://www.docker.com/) version 17.03+.
- [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/) (互換性のあるVersionのKubernetes clusterへ接続が必要)
- (Docker registry (ローカルのみで開発し、ImageをPushしないのであれば `docker-push`を適宜外し、ローカルのtag名にする))

参照: [Installation Guide](https://sdk.operatorframework.io/docs/building-operators/golang/installation/)


# 実行環境

- operator-sdk: `v.1.5.0`
- go: `1.16.3`
- docker: `20.10.5`
- kubernetes (Docker for Mac): `v1.19.7`


# ステップ

## 1. [Projectの作成](https://sdk.operatorframework.io/docs/building-operators/golang/tutorial/#create-a-new-project) (operator-sdk )

ディレクトリの作成

```
mkdir -p $HOME/projects/memcached-operator
cd $HOME/projects/memcached-operator
```

initializeする

```
operator-sdk init --domain example.com --repo github.com/example/memcached-operator
```

## 2. APIの作成 (operator-sdk)

KubernetesのAPI ResourceとControllerを作成 

```
operator-sdk create api --group cache --version v1alpha1 --kind Memcached --resource --controller
```

<details><summary>確認 (Optional)</summary><div>

この時点でのOperatorを確認したい場合は以下のようにDeployができる (ステップごとにOperatorがどの機能ができてどの機能ができていないのかを確認するためなので、スキップも可)

1. `OPERATOR_IMG`をセット

    ```
    docker_hub_name=nakamasato # 自分のDocker Hubのユーザ名を指定 (memcached-operatorというRepositoryは事前に作成しておく)
    export OPERATOR_IMG="$docker_hub_name/memcached-operator:v0.0.1-api-created"
    ```

1. Dockerイメージを焼いてPushする

    ```
    make docker-build docker-push IMG=$OPERATOR_IMG
    ```

1. OperatorをDeploy

    ```
    make deploy IMG=$OPERATOR_IMG
    ```


1. OperatorのPodがRunningしていることを確認

    ```
    kubectl get pod -n memcached-operator-system
    NAME                                                     READY   STATUS    RESTARTS   AGE
    memcached-operator-controller-manager-865ffbbbcc-pdkxg   2/2     Running   0          76s
    ```

1. Custom ResourceのMemcachedをデプロイ

    ```
    kubectl apply -f config/samples/cache_v1alpha1_memcached.yaml
    ```

1. Controllerの実装をしていないので、Deploymentは作成されていない

    ```
    kubectl get deploy   
    No resources found in default namespace.
    ```

1. Operator側のログを確認 (特に新しいMemcachedを作ったEventに対応するログはない)

    ```
    kubectl logs $(kubectl get po -n memcached-operator-system | grep memcached-operator-controller-manager | awk '{print $1}') -c manager -n memcached-operator-system -f
    2021-04-12T23:04:01.886Z	INFO	controller-runtime.metrics	metrics server is starting to listen	{"addr": "127.0.0.1:8080"}
    2021-04-12T23:04:01.887Z	INFO	setup	starting manager
I0412 23:04:01.982492       1 leaderelection.go:243] attempting to acquire leader lease  memcached-operator-system/86f835c3.example.com...
    2021-04-12T23:04:02.078Z	INFO	controller-runtime.manager	starting metrics server	{"path": "/metrics"}
    2021-04-12T23:04:02.282Z	DEBUG	controller-runtime.manager.events	Normal	{"object": {"kind":"ConfigMap","namespace":"memcached-operator-system","name":"86f835c3.example.com","uid":"dd829320-b9fb-4abc-9c18-cde6658daa96","apiVersion":"v1","resourceVersion":"2057764"}, "reason": "LeaderElection", "message": "memcached-operator-controller-manager-865ffbbbcc-pdkxg_4ecfda57-7d6a-42cc-bd5c-0cfa018a35f8 became leader"}
    2021-04-12T23:04:02.282Z	DEBUG	controller-runtime.manager.events	Normal	{"object": {"kind":"Lease","namespace":"memcached-operator-system","name":"86f835c3.example.com","uid":"2324fd70-20db-49a8-a975-98d71a158bd9","apiVersion":"coordination.k8s.io/v1","resourceVersion":"2057766"}, "reason": "LeaderElection", "message": "memcached-operator-controller-manager-865ffbbbcc-pdkxg_4ecfda57-7d6a-42cc-bd5c-0cfa018a35f8 became leader"}
    I0412 23:04:02.281814       1 leaderelection.go:253] successfully acquired lease memcached-operator-system/86f835c3.example.com
    2021-04-12T23:04:02.379Z	INFO	controller-runtime.manager.controller.memcached	Starting EventSource	{"reconciler group": "cache.example.com", "reconciler kind": "Memcached", "source": "kind source: /, Kind="}
    2021-04-12T23:04:02.479Z	INFO	controller-runtime.manager.controller.memcached	Starting Controller	{"reconciler group": "cache.example.com", "reconciler kind": "Memcached"}
    2021-04-12T23:04:02.479Z	INFO	controller-runtime.manager.controller.memcached	Starting workers	{"reconciler group": "cache.example.com", "reconciler kind": "Memcached", "worker count": 1}
    ```

</div></details>

ここまででできているもの:

|タイプ|機能|実装|
|---|---|---|
|APIリソース|MemcachedのAPI定義 (SpecのsizeとstatusのNodes)|No|
|Controller|Memcachedを取得|No|
|Controller|MemcachedのDeploymentが存在していない場合に作成|No|
|Controller|DeploymentのsizeをCustome Resource `Memcached`のspecで指定された値と同じにする|No|
|Controller|Custom Resource `Memcached`の statusを memcached のpod名で更新する|No|

## 3. APIを定義 (api/v1alpha1/memcached_types.go)

自動生成された `api/v1alpha1/memcached_types.go` を以下の様に変更する。

定義内容:

- MemcachedSpec: MemcachedがデプロイされたときにいくつのPodsで動かすかを指定するための`size`
- MemcachedStatus: Podの名前をMemcachedのNodesに文字列のリストとしてStatusにいれる
- Memcached: 上記のSpecとStatusを使って構造体を定義

```go
// MemcachedSpec defines the desired state of Memcached
type MemcachedSpec struct {
	//+kubebuilder:validation:Minimum=0
	// Size is the size of the memcached deployment
	Size int32 `json:"size"`
}

// MemcachedStatus defines the observed state of Memcached
type MemcachedStatus struct {
	// Nodes are the names of the memcached pods
	Nodes []string `json:"nodes"`
}
```

```go
// Memcached is the Schema for the memcacheds API
//+kubebuilder:subresource:status
type Memcached struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   MemcachedSpec   `json:"spec,omitempty"`
	Status MemcachedStatus `json:"status,omitempty"`
}
```

変更したあとには、これらを元に以下のコマンドで自動生成されたコードを更新する

```
make generate
```

API定義後にCRDのManifestファイルを以下のコマンドで生成

```
make manifests
```


<details><summary>確認 (Optional)</summary><div>

この時点でのOperatorを確認したい場合は以下のようにDeployができる (ステップごとにOperatorがどの機能ができてどの機能ができていないのかを確認するためなので、スキップも可)

1. `OPERATOR_IMG`をセット

    ```
    docker_hub_name=nakamasato # 自分のDocker Hubのユーザ名を指定 (memcached-operatorというRepositoryは事前に作成しておく)
    export OPERATOR_IMG="$docker_hub_name/memcached-operator:v0.0.1-api-defined"
    ```

1. Dockerイメージを焼いてPushする

    ```
    make docker-build docker-push IMG=$OPERATOR_IMG
    ```

1. OperatorをDeploy

    ```
    make deploy IMG=$OPERATOR_IMG
    ```


1. OperatorのPodが新しいimageでRunningしていることを確認

    ```
    kubectl get pod -n memcached-operator-system      
    NAME                                                    READY   STATUS    RESTARTS   AGE
    memcached-operator-controller-manager-bcdb99b56-n4l5c   2/2     Running   0          24s
    ```

    イメージの確認: `v0.0.1-api-defined`となってること

    ```
    kubectl get pod -n memcached-operator-system -o yaml | grep ' image:'
          image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0
          image: nakamasato/memcached-operator:v0.0.1-api-defined
          image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0
          image: nakamasato/memcached-operator:v0.0.1-api-defined
    ```

1. Custom ResourceのMemcachedをデプロイ (一つ前の確認でApplyした場合は同じなので必要ない)

    ```
    kubectl apply -f config/samples/cache_v1alpha1_memcached.yaml
    ```

1. Controllerの実装をしていないので、Deploymentは作成されていない

    ```
    kubectl get deploy   
    No resources found in default namespace.
    ```

1. Operator側のログを確認 (特に新しいMemcachedを作ったEventに対応するログはない)

    ```
    kubectl logs $(kubectl get po -n memcached-operator-system | grep memcached-operator-controller-manager | awk '{print $1}') -c manager -n memcached-operator-system -f
    2021-04-12T23:21:10.637Z        INFO    controller-runtime.metrics      metrics server is starting to listen    {"addr": "127.0.0.1:8080"}
    2021-04-12T23:21:10.639Z        INFO    setup   starting manager
    I0412 23:21:10.640597       1 leaderelection.go:243] attempting to acquire leader lease  memcached-operator-system/86f835c3.example.com...
    2021-04-12T23:21:10.640Z        INFO    controller-runtime.manager      starting metrics server {"path": "/metrics"}
    I0412 23:21:38.610611       1 leaderelection.go:253] successfully acquired lease memcached-operator-system/86f835c3.example.com
    2021-04-12T23:21:38.610Z        DEBUG   controller-runtime.manager.events       Normal  {"object": {"kind":"ConfigMap","namespace":"memcached-operator-system","name":"86f835c3.example.com","uid":"dd829320-b9fb-4abc-9c18-cde6658daa96","apiVersion":"v1","resourceVersion":"2060588"}, "reason": "LeaderElection", "message": "memcached-operator-controller-manager-bcdb99b56-n4l5c_3302a23e-cab6-4527-a187-f19eeb9fedbb became leader"}
    2021-04-12T23:21:38.610Z        DEBUG   controller-runtime.manager.events       Normal  {"object": {"kind":"Lease","namespace":"memcached-operator-system","name":"86f835c3.example.com","uid":"2324fd70-20db-49a8-a975-98d71a158bd9","apiVersion":"coordination.k8s.io/v1","resourceVersion":"2060589"}, "reason": "LeaderElection", "message": "memcached-operator-controller-manager-bcdb99b56-n4l5c_3302a23e-cab6-4527-a187-f19eeb9fedbb became leader"}
    2021-04-12T23:21:38.611Z        INFO    controller-runtime.manager.controller.memcached Starting EventSource    {"reconciler group": "cache.example.com", "reconciler kind": "Memcached", "source": "kind source: /, Kind="}
    2021-04-12T23:21:39.001Z        INFO    controller-runtime.manager.controller.memcached Starting Controller     {"reconciler group": "cache.example.com", "reconciler kind": "Memcached"}
    2021-04-12T23:21:39.001Z        INFO    controller-runtime.manager.controller.memcached Starting workers        {"reconciler group": "cache.example.com", "reconciler kind": "Memcached", "worker count": 1}
    ```

</div>
</details>

ここまででできているもの:

|タイプ|機能|実装|
|---|---|---|
|APIリソース|MemcachedのAPI定義 (SpecのsizeとstatusのNodes)|Yes|
|Controller|Memcachedを取得|No|
|Controller|MemcachedのDeploymentが存在していない場合に作成|No|
|Controller|DeploymentのsizeをCustome Resource `Memcached`のspecで指定された値と同じにする|No|
|Controller|Custom Resource `Memcached`の statusを memcached のpod名で更新する|No|

## 4.1. コントローラーの実装 (MemcachedのInstanceを取得)

`Reconcile` に Memcached を取得するロジックを追加 （ログで自分で書いたコードを判別しやすくするため `1. Fetch the Memcached instance.`をすべてのログメッセージに書いている）

```go:controllers/memcached_controller.go
func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := r.Log.WithValues("memcached", req.NamespacedName)

	// 1. Fetch the Memcached instance
	memcached := &cachev1alpha1.Memcached{}
	err := r.Get(ctx, req.NamespacedName, memcached)
	if err != nil {
		if errors.IsNotFound(err) {
			log.Info("1. Fetch the Memcached instance. Memcached resource not found. Ignoring since object must be deleted")
			return ctrl.Result{}, nil
		}
		// Error reading the object - requeue the request.
		log.Error(err, "1. Fetch the Memcached instance. Failed to get Mmecached")
		return ctrl.Result{}, err
	}
	log.Info("1. Fetch the Memcached instance. Memchached resource found", "memcached.Name", memcached.Name, "memcached.Namespace", memcached.Namespace)	

	return ctrl.Result{}, nil
}
```

<details><summary>確認 (Optional)</summary><div>

この時点でのOperatorを確認したい場合は以下のようにDeployができる (ステップごとにOperatorがどの機能ができてどの機能ができていないのかを確認するためなので、スキップも可)

1. `OPERATOR_IMG`をセット

    ```
    docker_hub_name=nakamasato # 自分のDocker Hubのユーザ名を指定 (memcached-operatorというRepositoryは事前に作成しておく)
    export OPERATOR_IMG="$docker_hub_name/memcached-operator:v0.0.1-controller-fetch-memcached-instance"
    ```

1. Dockerイメージを焼いてPushする

    ```
    make docker-build docker-push IMG=$OPERATOR_IMG
    ```

1. OperatorをDeploy

    ```
    make deploy IMG=$OPERATOR_IMG
    ```


1. OperatorのPodが新しいimageでRunningしていることを確認

    ```
    kubectl get pod -n memcached-operator-system      
    ```

イメージの確認: `v0.0.1-controller-fetch-memcached-instance`となってること

    ```
    kubectl get pod -n memcached-operator-system -o yaml | grep ' image:'
    ```

1. Custom ResourceのMemcachedをデプロイ (一つ前の確認でApplyした場合は同じなので必要ない)

    ```
    kubectl apply -f config/samples/cache_v1alpha1_memcached.yaml
    ```

1. Controllerの実装をしていないので、Deploymentは作成されていない

    ```
    kubectl get deploy   
    No resources found in default namespace.
    ```

1. Operator側のログを確認 (`1. Fetch the Memcached instance.`がログにかかれている!)

    ```
    kubectl logs $(kubectl get po -n memcached-operator-system | grep memcached-operator-controller-manager | awk '{print $1}') -c manager -n memcached-operator-system -f
    ...
    2021-04-12T23:35:02.751Z        INFO    controllers.Memcached   1. Fetch the Memcached instance. Memchached resource found      {"memcached": "default/memcached-sample", "memcached.Name": "memcached-sample", "memcached.Namespace": "default"}
    ```

</div>
</details>

ここまででできているもの:

|タイプ|機能|実装|
|---|---|---|
|APIリソース|MemcachedのAPI定義 (SpecのsizeとstatusのNodes)|Yes|
|Controller|Memcachedを取得|Yes|
|Controller|MemcachedのDeploymentが存在していない場合に作成|No|
|Controller|DeploymentのsizeをCustome Resource `Memcached`のspecで指定された値と同じにする|No|
|Controller|Custom Resource `Memcached`の statusを memcached のpod名で更新する|No|

## 4.2. コントローラーの実装 (Deploymentをなければ作成)

1. 必要なlibraryをImport
1. Controllerに必要なRBACをMarkerにより追加
1. NamespacedNameからDeploymentが存在しない場合 (`IsNotFound`)は`deployemntForMemcached`関数でDeploymentを作成
1. `deployemntForMemcached` 関数の定義 (DeploymentをMemcached用に作成する)
1. `labelsForMemcached`関数の定義 (新規Deployment作成時につけるlabelを返す)

```go:controllers/memcached_controllers.go
...

import (
	appsv1 "k8s.io/api/apps/v1" // 追加
	corev1 "k8s.io/api/core/v1" // 追加
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // 追加
	"k8s.io/apimachinery/pkg/types" // 追加

	"context"

        ...
)

...

//+kubebuilder:rbac:groups=cache.example.com,resources=memcacheds,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/finalizers,verbs=update
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete // 追加
//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list; // 追加

...

func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := r.Log.WithValues("memcached", req.NamespacedName)

	// 1. Fetch the Memcached instance
        // ...前節で実装済み

	// 2. Check if the deployment already exists, if not create a new one
	found := &appsv1.Deployment{}
	err = r.Get(ctx, types.NamespacedName{Name: memcached.Name, Namespace: memcached.Namespace}, found)
	if err != nil && errors.IsNotFound(err) {
		// Define a new deployment
		dep := r.deploymentForMemcached(memcached)
		log.Info("2. Check if the deployment already exists, if not create a new one. Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
		err = r.Create(ctx, dep)
		if err != nil {
			log.Error(err, "2. Check if the deployment already exists, if not create a new one. Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
			return ctrl.Result{}, err
		}
		// Deployment created successfully - return and requeue
		return ctrl.Result{Requeue: true}, nil
	} else if err != nil {
		log.Error(err, "2. Check if the deployment already exists, if not create a new one. Failed to get Deployment")
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

// deploymentForMemcached returns a memcached Deployment object
func (r *MemcachedReconciler) deploymentForMemcached(m *cachev1alpha1.Memcached) *appsv1.Deployment {
	ls := labelsForMemcached(m.Name)
	replicas := m.Spec.Size

	dep := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      m.Name,
			Namespace: m.Namespace,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &replicas,
			Selector: &metav1.LabelSelector{
				MatchLabels: ls,
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: ls,
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{{
						Image:   "memcached:1.4.36-alpine",
						Name:    "memcached",
						Command: []string{"memcached", "-m=64", "-o", "modern", "-v"},
						Ports: []corev1.ContainerPort{{
							ContainerPort: 11211,
							Name:          "memcached",
						}},
					}},
				},
			},
		},
	}
	// Set Memcached instance as the owner and controller
	ctrl.SetControllerReference(m, dep, r.Scheme)
	return dep
}

// labelsForMemcached returns the labels for selecting the resources
// belonging to the given memcached CR name.
func labelsForMemcached(name string) map[string]string {
	return map[string]string{"app": "memcached", "memcached_cr": name}
}

...
```

`SetupWithManager`で、どのリソースをWatchするかを設定している。今回作成した `Memcached` 以外にも管理したいDeploymentを`Owns`で管理できるようにする。

```go:memcached_controller.go
// SetupWithManager sets up the controller with the Manager.
func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&cachev1alpha1.Memcached{}).
		Owns(&appsv1.Deployment{}).
		Complete(r)
}
```

<details><summary>確認 (Optional)</summary><div>

この時点でのOperatorを確認したい場合は以下のようにDeployができる (ステップごとにOperatorがどの機能ができてどの機能ができていないのかを確認するためなので、スキップも可)

1. `OPERATOR_IMG`をセット

    ```
    docker_hub_name=nakamasato # 自分のDocker Hubのユーザ名を指定 (memcached-operatorというRepositoryは事前に作成しておく)
    export OPERATOR_IMG="$docker_hub_name/memcached-operator:v0.0.1-controller-create-memcached-deployment"
    ```

1. Dockerイメージを焼いてPushする

    ```
    make docker-build docker-push IMG=$OPERATOR_IMG
    ```

    以下のエラーがでたら 指示通りに`go get k8s.io/api/core/v1@v0.19.2`を実行する

    ```
     go: github.com/example/memcached-operator/controllers: package k8s.io/api/core/v1 imported from implicitly required module; to add missing requirements, run:
            go get k8s.io/api/core/v1@v0.19.2
    ```

1. OperatorをDeploy

    ```
    make deploy IMG=$OPERATOR_IMG
    ```


1. OperatorのPodが新しいimageでRunningしていることを確認

    ```
    kubectl get pod -n memcached-operator-system      
    ```

イメージの確認: `v0.0.1-controller-create-memcached-deployment`となってること

    ```
    kubectl get pod -n memcached-operator-system -o yaml | grep ' image:'
    ```

1. Custom ResourceのMemcachedをデプロイ

    Fieldが変わっているので一度削除してからApplyし直す

    ```
    kubectl delete -f config/samples/cache_v1alpha1_memcached.yaml
    ```

    以下のようにSpecを`size: 3`に変更

    ```yaml:config/samples/cache_v1alpha1_memcached.yaml
    apiVersion: cache.example.com/v1alpha1
    kind: Memcached
    metadata:
      name: memcached-sample
    spec:
      size: 3
    ```

    ```
    kubectl apply -f config/samples/cache_v1alpha1_memcached.yaml
    ```

1. ControllerでDeploymentを作成する部分を実装したのでDeploymentができている

    ```
    kubectl get deploy   
    NAME               READY   UP-TO-DATE   AVAILABLE   AGE
    memcached-sample   3/3     3            3           4s
    ```

    しかしspecのサイズを替えても、Deploymentには反映しない。 (次の節で実装) (試したい場合は、`kubectl patch memcached memcached-sample -p '{"spec":{"size": 5}}' --type=merge` をアプライしてから、 `kubectl get deploy`で数が変わらないことを確認)

1. Operator側のログを確認 (`2. Check if the deployment already exists, if not create a new one. Creating a new Deployment`がログにかかれている!)

    ```
    kubectl logs $(kubectl get po -n memcached-operator-system | grep memcached-operator-controller-manager | awk '{print $1}') -c manager -n memcached-operator-system -f
    ...
    2021-04-13T00:16:21.687Z        INFO    controllers.Memcached   1. Fetch the Memcached instance. Memchached resource found      {"memcached": "default/memcached-sample", "memcached.Name": "memcached-sample", "memcached.Namespace": "default"}
    2021-04-13T00:16:21.982Z        INFO    controllers.Memcached   2. Check if the deployment already exists, if not create a new one. Creating a new Deployment   {"memcached": "default/memcached-sample", "Deployment.Namespace": "default", "Deployment.Name": "memcached-sample"}
    2021-04-13T00:16:22.006Z        INFO    controllers.Memcached   1. Fetch the Memcached instance. Memchached resource found      {"memcached": "default/memcached-sample", "memcached.Name": "memcached-sample", "memcached.Namespace": "default"}
    ```

</div>
</details>

ここまででできているもの:

|タイプ|機能|実装|
|---|---|---|
|APIリソース|MemcachedのAPI定義 (SpecのsizeとstatusのNodes)|Yes|
|Controller|Memcachedを取得|Yes|
|Controller|MemcachedのDeploymentが存在していない場合に作成|Yes|
|Controller|DeploymentのsizeをCustome Resource `Memcached`のspecで指定された値と同じにする|No|
|Controller|Custom Resource `Memcached`の statusを memcached のpod名で更新する|No|


## 4.3. コントローラーの実装 (Deployment sizeとMemcached sizeを同じ値に保つ)

```go:controllers/memcached_controllers.go
func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	// 1. Fetch the Memcached instance
	// 実装済み
	// 2. Check if the deployment already exists, if not create a new one
	// 実装済み
	// 3. Ensure the deployment size is the same as the spec
	size := memcached.Spec.Size
	if *found.Spec.Replicas != size {
		found.Spec.Replicas = &size
		err = r.Update(ctx, found)
		if err != nil {
			log.Error(err, "3. Ensure the deployment size is the same as the spec. Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
			return ctrl.Result{}, err
		}
		// Spec updated - return and requeue
		log.Info("3. Ensure the deployment size is the same as the spec. Update deployment size", "Deployment.Spec.Replicas", size)
		return ctrl.Result{Requeue: true}, nil
	}
```

<details><summary>確認 (Optional)</summary><div>

この時点でのOperatorを確認したい場合は以下のようにDeployができる (ステップごとにOperatorがどの機能ができてどの機能ができていないのかを確認するためなので、スキップも可)

1. `OPERATOR_IMG`をセット

    ```
    docker_hub_name=nakamasato # 自分のDocker Hubのユーザ名を指定 (memcached-operatorというRepositoryは事前に作成しておく)
    export OPERATOR_IMG="$docker_hub_name/memcached-operator:v0.0.1-controller-sync-deployment-size"
    ```

1. Dockerイメージを焼いてPushする

    ```
    make docker-build docker-push IMG=$OPERATOR_IMG
    ```

1. OperatorをDeploy

    ```
    make deploy IMG=$OPERATOR_IMG
    ```


1. OperatorのPodが新しいimageでRunningしていることを確認

    ```
    kubectl get pod -n memcached-operator-system      
    ```

イメージの確認: `v0.0.1-controller-sync-deployment-size`となってること

    ```
    kubectl get pod -n memcached-operator-system -o yaml | grep ' image:'
    ```

1. Custom ResourceのMemcachedをデプロイ

    以下のようにSpecを`size: 3` -> `size: 2`に変更

    ```yaml:config/samples/cache_v1alpha1_memcached.yaml
    apiVersion: cache.example.com/v1alpha1
    kind: Memcached
    metadata:
      name: memcached-sample
    spec:
      size: 2
    ```

    ```
    kubectl apply -f config/samples/cache_v1alpha1_memcached.yaml
    ```

1. ControllerでDeploymentを作成する部分を実装したReplicaが2に変更されている!

    ```
    kubectl get deploy   
    NAME               READY   UP-TO-DATE   AVAILABLE   AGE
    memcached-sample   2/2     2            2           4s
    ```

1. Operator側のログを確認 (`3. Ensure the deployment size is the same as the spec.`がログにかかれている!)

    ```
    kubectl logs $(kubectl get po -n memcached-operator-system | grep memcached-operator-controller-manager | awk '{print $1}') -c manager -n memcached-operator-system -f
    ...
    2021-04-13T00:42:26.366Z        INFO    controllers.Memcached   1. Fetch the Memcached instance. Memchached resource found      {"memcached": "default/memcached-sample", "memcached.Name": "memcached-sample", "memcached.Namespace": "default"}
    2021-04-13T00:42:26.377Z        INFO    controllers.Memcached   3. Ensure the deployment size is the same as the spec. Update deployment size   {"memcached": "default/memcached-sample", "Deployment.Spec.Replicas": 2}
    2021-04-13T00:42:26.383Z        INFO    controllers.Memcached   1. Fetch the Memcached instance. Memchached resource found      {"memcached": "default/memcached-sample", "memcached.Name": "memcached-sample", "memcached.Namespace": "default"}
    ```

</div>
</details>

ここまででできているもの:

|タイプ|機能|実装|
|---|---|---|
|APIリソース|MemcachedのAPI定義 (SpecのsizeとstatusのNodes)|Yes|
|Controller|Memcachedを取得|Yes|
|Controller|MemcachedのDeploymentが存在していない場合に作成|Yes|
|Controller|DeploymentのsizeをCustome Resource `Memcached`のspecで指定された値と同じにする|Yes|
|Controller|Custom Resource `Memcached`の statusを memcached のpod名で更新する|No|

## 4.4. コントローラーの実装 (Memcached statusを Pod名で更新)

- `reflect`をimportに追加
- ロジック追加: podListを取得→`getPodNames`からPod名のarrayを取得→`memcached.Status.Nodes`に反映させる
- `getPodNames`関数を定義: arrayにPod名を入れて返す



```go:controllers/memcached_controllers.go

import (
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"reflect" //追加

	"context"

	cachev1alpha1 "github.com/example/memcached-operator/api/v1alpha1"
	"github.com/go-logr/logr"
	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
)
...

func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := r.Log.WithValues("memcached", req.NamespacedName)

	// 1. Fetch the Memcached instance
        // 実装済み
	// 2. Check if the deployment already exists, if not create a new one
        // 実装済み
	// 3. Ensure the deployment size is the same as the spec
        // 実装済み

	// 4. Update the Memcached status with the pod names
	// List the pods for this memcached's deployment
	podList := &corev1.PodList{}
	listOpts := []client.ListOption{
		client.InNamespace(memcached.Namespace),
		client.MatchingLabels(labelsForMemcached(memcached.Name)),
	}
	if err = r.List(ctx, podList, listOpts...); err != nil {
		log.Error(err, "4. Update the Memcached status with the pod names. Failed to list pods", "Memcached.Namespace", memcached.Namespace, "Memcached.Name", memcached.Name)
		return ctrl.Result{}, err
	}
	podNames := getPodNames(podList.Items)
	log.Info("4. Update the Memcached status with the pod names. Pod list", "podNames", podNames)

	// Update status.Nodes if needed
	if !reflect.DeepEqual(podNames, memcached.Status.Nodes) {
		memcached.Status.Nodes = podNames
		err := r.Status().Update(ctx, memcached)
		if err != nil {
			log.Error(err, "4. Update the Memcached status with the pod names. Failed to update Memcached status")
			return ctrl.Result{}, err
		}
	}
	log.Info("4. Update the Memcached status with the pod names. Update memcached.Status", "memcached.Status.Nodes", memcached.Status.Nodes)

	return ctrl.Result{}, nil
}

...

// getPodNames returns the pod names of the array of pods passed in
func getPodNames(pods []corev1.Pod) []string {
	var podNames []string
	for _, pod := range pods {
		podNames = append(podNames, pod.Name)
	}
	return podNames
}
```

<details><summary>確認 (Optional)</summary><div>

この時点でのOperatorを確認したい場合は以下のようにDeployができる (ステップごとにOperatorがどの機能ができてどの機能ができていないのかを確認するためなので、スキップも可)

1. `OPERATOR_IMG`をセット

    ```
    docker_hub_name=nakamasato # 自分のDocker Hubのユーザ名を指定 (memcached-operatorというRepositoryは事前に作成しておく)
    export OPERATOR_IMG="$docker_hub_name/memcached-operator:v0.0.1-controller-update-memcached-status"
    ```

1. Dockerイメージを焼いてPushする

    ```
    make docker-build docker-push IMG=$OPERATOR_IMG
    ```

1. OperatorをDeploy

    ```
    make deploy IMG=$OPERATOR_IMG
    ```


1. OperatorのPodが新しいimageでRunningしていることを確認

    ```
    kubectl get pod -n memcached-operator-system      
    ```

イメージの確認: `v0.0.1-controller-update-memcached-status`となってること

    ```
    kubectl get pod -n memcached-operator-system -o yaml | grep ' image:'
    ```

1. Custom ResourceのMemcachedをデプロイ (前節でDeployしてあれば変更なし)

    ```
    kubectl apply -f config/samples/cache_v1alpha1_memcached.yaml
    ```

1. Memcachedのstatus.nodesがpod名になっていることを確認

    ```
    kubectl get memcached memcached-sample -o jsonpath='{.status}'
    {"nodes":["memcached-sample-6c765df685-5b9dm","memcached-sample-6c765df685-z5zgn"]}
    ```

    ```
    kubectl get pod                                                      
    NAME                                READY   STATUS    RESTARTS   AGE
    memcached-sample-6c765df685-5b9dm   1/1     Running   0          37m
    memcached-sample-6c765df685-z5zgn   1/1     Running   0          37m
    ```

1. Operator側のログを確認 (``がログにかかれている!)

    ```
    kubectl logs $(kubectl get po -n memcached-operator-system | grep memcached-operator-controller-manager | awk '{print $1}') -c manager -n memcached-operator-system -f
    ...
    2021-04-13T00:57:17.594Z        INFO    controllers.Memcached   1. Fetch the Memcached instance. Memchached resource found      {"memcached": "default/memcached-sample", "memcached.Name": "memcached-sample", "memcached.Namespace": "default"}
    2021-04-13T00:57:17.660Z        INFO    controllers.Memcached   4. Update the Memcached status with the pod names. Pod list     {"memcached": "default/memcached-sample", "podNames": ["memcached-sample-6c765df685-5b9dm", "memcached-sample-6c765df685-z5zgn"]}
    2021-04-13T00:57:17.660Z        INFO    controllers.Memcached   4. Update the Memcached status with the pod names. Update memcached.Status      {"memcached": "default/memcached-sample", "memcached.Status.Nodes": ["memcached-sample-6c765df685-5b9dm", "memcached-sample-6c765df685-z5zgn"]}
    ```

</div>
</details>

ここまででできているもの:

|タイプ|機能|実装|
|---|---|---|
|APIリソース|MemcachedのAPI定義 (SpecのsizeとstatusのNodes)|Yes|
|Controller|Memcachedを取得|Yes|
|Controller|MemcachedのDeploymentが存在していない場合に作成|Yes|
|Controller|DeploymentのsizeをCustome Resource `Memcached`のspecで指定された値と同じにする|Yes|
|Controller|Custom Resource `Memcached`の statusを memcached のpod名で更新する|Yes|

## 5. 確認

1. もしもMemcachedを前節でDeployしていれば削除。

    ```bash
    kubectl delete -f config/samples/cache_v1alpha1_memcached.yaml
    ```

1. memcached-operatorのデプロイ　 -> operatorは `memcached-operator-system` というNamespaceにデプロイされる

    ```bash
    export OPERATOR_IMG="nakamasato/memcached-operator:v0.0.1"
    make deploy IMG=$OPERATOR_IMG
    ```

1. Memcached (CR)を作成

    ```bash
    kubectl apply -f config/samples/cache_v1alpha1_memcached.yaml
    ```

    ControllerのLogから以下が見て取れる:
　　 1. Memcachedの取得
　　 2. Deploymentの作成
　　 3. MemcachedStatusの更新

    ```bash
    kubectl logs $(kubectl get po -n memcached-operator-system | grep memcached-operator-controller-manager | awk '{print $1}') -c manager -n memcached-operator-system -f
    ```

    ```bash
    2021-11-12T00:47:04.417Z        INFO    controllers.Memcached   1. Fetch the Memcached instance. Memchached resource found      {"memcached": "default/memcached-sample", "memcached.Name": "memcached-sample", "memcached.Namespace": "default"}
    2021-11-12T00:47:04.417Z        INFO    controllers.Memcached   2. Check if the deployment already exists, if not create a new one. Creating a new Deployment   {"memcached": "default/memcached-sample", "Deployment.Namespace": "default", "Deployment.Name": "memcached-sample"}
    2021-11-12T00:47:04.422Z        INFO    controllers.Memcached   1. Fetch the Memcached instance. Memchached resource found      {"memcached": "default/memcached-sample", "memcached.Name": "memcached-sample", "memcached.Namespace": "default"}
    2021-11-12T00:47:04.523Z        INFO    controllers.Memcached   4. Update the Memcached status with the pod names. Pod list     {"memcached": "default/memcached-sample", "podNames": ["memcached-sample-6c765df685-n7ml4", "memcached-sample-6c765df685-74l72"]}
    2021-11-12T00:47:04.529Z        INFO    controllers.Memcached   4. Update the Memcached status with the pod names. Update memcached.Status      {"memcached": "default/memcached-sample", "memcached.Status.Nodes": ["memcached-sample-6c765df685-n7ml4", "memcached-sample-6c765df685-74l72"]}
    ```

1. Memcached のサイズ変更

    ```bash
    kubectl patch memcached memcached-sample -p '{"spec":{"size": 5}}' --type=merge
    ```

    ログを確認:
　　     1. Memcachedの取得
    　　 2. DeploymentのReplica更新
    　　 3. MemcachedStatusの更新


    ```bash
    kubectl logs $(kubectl get po -n memcached-operator-system | grep memcached-operator-controller-manager | awk '{print $1}') -c manager -n memcached-operator-system -f
    ```


    ```bash
    2021-11-12T00:48:44.764Z        INFO    controllers.Memcached   1. Fetch the Memcached instance. Memchached resource found      {"memcached": "default/memcached-sample", "memcached.Name": "memcached-sample", "memcached.Namespace": "default"}
    2021-11-12T00:48:44.772Z        INFO    controllers.Memcached   3. Ensure the deployment size is the same as the spec. Update deployment size   {"memcached": "default/memcached-sample", "Deployment.Spec.Replicas": 5}
    2021-11-12T00:48:44.772Z        INFO    controllers.Memcached   1. Fetch the Memcached instance. Memchached resource found      {"memcached": "default/memcached-sample", "memcached.Name": "memcached-sample", "memcached.Namespace": "default"}
    2021-11-12T00:48:44.772Z        INFO    controllers.Memcached   4. Update the Memcached status with the pod names. Pod list     {"memcached": "default/memcached-sample", "podNames": ["memcached-sample-6c765df685-n7ml4", "memcached-sample-6c765df685-74l72"]}
    ...
    2021-11-12T00:48:45.987Z        INFO    controllers.Memcached   4. Update the Memcached status with the pod names. Pod list     {"memcached": "default/memcached-sample", "podNames": ["memcached-sample-6c765df685-n7ml4", "memcached-sample-6c765df685-74l72", "memcached-sample-6c765df685-g9pj5", "memcached-sample-6c765df685-9wjrv", "memcached-sample-6c765df685-nbbmk"]}
2021-11-12T00:48:45.987Z        INFO    controllers.Memcached   4. Update the Memcached status with the pod names. Update memcached.Status      {"memcached": "default/memcached-sample", "memcached.Status.Nodes": ["memcached-sample-6c765df685-n7ml4", "memcached-sample-6c765df685-74l72", "memcached-sample-6c765df685-g9pj5", "memcached-sample-6c765df685-9wjrv", "memcached-sample-6c765df685-nbbmk"]}
    ```

    `3. Ensure the deployment size is the same as the spec. Update deployment size`で、Deploymentの数が更新されていることがわかる
    さらに、`4. Update the Memcached status with the pod names. Update memcached.Status      {"memcached": "default/memcached-sample", "memcached.Status.Nodes": ["memcached-sample-6c765df685-n7ml4", "memcached-sample-6c765df685-74l72", "memcached-sample-6c765df685-g9pj5", "memcached-sample-6c765df685-9wjrv", "memcached-sample-6c765df685-nbbmk"]}` で Status.Nodesにも反映されていることがわかる

    ```bash
kubectl get po 
NAME                                READY   STATUS    RESTARTS   AGE
memcached-sample-6c765df685-74l72   1/1     Running   0          4m36s
memcached-sample-6c765df685-9wjrv   1/1     Running   0          2m56s
memcached-sample-6c765df685-g9pj5   1/1     Running   0          2m56s
memcached-sample-6c765df685-n7ml4   1/1     Running   0          4m36s
memcached-sample-6c765df685-nbbmk   1/1     Running   0          2m56s
    ```

    ```bash
    kubectl get memcached memcached-sample -o jsonpath='{.status}'
    {"nodes":["memcached-sample-6c765df685-n7ml4","memcached-sample-6c765df685-74l72","memcached-sample-6c765df685-g9pj5","memcached-sample-6c765df685-9wjrv","memcached-sample-6c765df685-nbbmk"]}
    ```

1. Memcachedの削除

    ```bash
    kubectl delete -f config/samples/cache_v1alpha1_memcached.yaml
    ```

    ログを確認すると削除された

    ```bash
    2021-11-12T00:53:22.424Z        INFO    controllers.Memcached   1. Fetch the Memcached instance. Memcached resource not found. Ignoring since object must be deleted    {"memcached": "default/memcached-sample"}
    ```

## 6. operatorの削除

```
make undeploy
```

# まとめ

- Operator-sdkのTutorialで、初心者でも一歩ずつ開発からDeployして確認までのプロセスを学べた
- Code: https://github.com/nakamasato/memcached-operator