KubernetesのOperatorを手探りで作ってみる

## 概要

Kubernetes API の機能を拡張したものとして「Operator」と呼ばれるものがあるようで、使えると結構便利らしい。が、記事を読んだりしてみても一体どんな風に便利に使えるのかがなかなかピンと来ない…。こんな時は「手を動かしてなんぼ」なのかなあと思い始めて、Operatorのサンプルを動かしてみることにした。本記事では、初めてKubernetesのOperatorを作成する筆者がそれなりにつまづきながら試行錯誤した内容をまとめる。

Operatorを実装できるツールは幾つかあるみたいだが、一番有名なのは「Operator Framework」のようだ。以下サイトの "Quickstart for Go-based Operators" の手順に従えば、MemcachedのカスタムリソースとOperatorを「とりあえず形だけ」作れるようなので試してみることにする。

https://sdk.operatorframework.io/docs/building-operators/golang/quickstart/

## 本記事の対象者

主にKubernetesのOperatorを初めて触ってみる方で、かつ**Push可能なDocker Registryを持っていない人向け**。
途中で「作ったOperatorをDocker RegistryにPush＆Pullする」手順が出てくるが、PushできるDocker Registryを持っていないと、筆者がそうであったようにここでつまづいてしまう。その際の対処法として、Docker Registryを自作する方法と関連する設定を記載している。
逆にPush可能なDocker Registryをお持ちの方にとっては余計な手順が沢山書かれているので、素直に[Quickstart for Go-based Operators](https://sdk.operatorframework.io/docs/building-operators/golang/quickstart/)の手順を実行したほうがいいと思われる。

## 環境

* Kubernetes（kubectl等）
    * バージョン : v1.24.3
* クラスター構成
    * control plane（master）
        * OS : Debian GNU/Linux 10
        * CPUアーキテクチャ : x86_64
        * node（worker）も兼務
    * node（worker）
        * OS : Ubuntu 20.04.4 LTS
        * CPUアーキテクチャ : x86_64
* Go言語
    * バージョン : go1.18.4
* Operator SDK CLI
    * バージョン : v1.22.2

※以後の作業は、特段記載がない場合control planeで行う。

## 必要な準備

* [Kubernetesのインストール](https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/install-kubeadm/)
    * オーソドックスに、kubeadmを使ってインストール
    * 複数マシンでクラスターを作る場合は、ワーカーノードのjoinやマシン間のCNI設定が必要
* [Go言語のインストール](https://go.dev/doc/install)
    * ダウンロードしたバイナリを配置してPATHを通すだけなので、そんなに苦労しないはず
* [Operator SDK CLIのインストール](https://sdk.operatorframework.io/docs/installation/)
    * Linuxの場合、「Install from GitHub release」の手順で行ける
* [Dockerのインストール](https://docs.docker.com/engine/install/)
    * OSに応じて適宜、上記サイトの各リンクを参照
    * Kubernetesのv1.24以降はコンテナランタイムにDockerを標準で使わなくなった（containerdのみで動く！）みたいだが、Operatorの作成でDockerが必要となる。
* **Pushが可能なDocker Registry**
    * 作ったOperatorを格納するのに必要！
    * <font color="Crimson">**本記事はこれを持っていない方向け**</font>

#### Docker Registryをローカル環境に作成

必要な準備の最後、「Pushが可能なDocker Registry」を筆者は持っていなかった…。ここが準備できていないと、[Quickstart](https://sdk.operatorframework.io/docs/building-operators/golang/quickstart/)の「Build and push your operator’s image」で作ったDockerイメージがPushできずに困ることになる。

そこでまずは以下コマンドで、ローカル環境（control planeでなくてもいい）にDocker Registryを作成する。

```
$ docker run -d --name docker-registry -p 5000:5000 registry
```

一応、IPを名前解決できるように、control planeの``/etc/hosts``に以下を追記

```
a.b.c.d  [hostname-registry]
```

* ``a.b.c.d``はDocker RegistryのホストOSのIPアドレスの記載が必要
* ``[hostname-registry]``は任意のホスト名を記載

念のためDocker Registryが動いているか確認。

```
$ curl http://[hostname-registry]:5000/v2/_catalog
{"repositories":[]}
```

## memcached-operatorのビルド

[Quickstart](https://sdk.operatorframework.io/docs/building-operators/golang/quickstart/)のStepsに従ってセットアップしていく。

### (1) プロジェクトの初期化

```
$ mkdir memcached-operator
$ cd memcached-operator
$ operator-sdk init --domain [hostname-registry] --repo github.com/example/memcached-operator
```

コマンドを実行すると、以下のような構成になる。
config/default配下等に、k8sのマニフェストと思われるyamlファイルが格納されている。

```
$ tree -L 2 .
.
├── Dockerfile
├── Makefile
├── PROJECT
├── README.md
├── config
│   ├── default
│   ├── manager
│   ├── manifests
│   ├── prometheus
│   ├── rbac
│   └── scorecard
├── go.mod
├── go.sum
├── hack
│   └── boilerplate.go.txt
└── main.go
```

### (2) Memcached APIの作成

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

再びディレクトリ構成を見ると、以下のように*印部分が追加されている。おそらくはここで、CustomResourceDefinition(CRD)のマニフェスト、およびカスタムコントローラーを作るための資材が作られていると思われる。

```
$ tree -L 2 .
.
├── Dockerfile
├── Makefile
├── PROJECT
├── README.md
├── api *
│   └── v1alpha1 *
├── bin *
│   └── controller-gen *
├── config
│   ├── crd *
│   ├── default
│   ├── manager
│   ├── manifests
│   ├── prometheus
│   ├── rbac
│   ├── samples *
│   └── scorecard
├── controllers *
│   ├── memcached_controller.go *
│   └── suite_test.go *
├── go.mod
├── go.sum
├── hack
│   └── boilerplate.go.txt
└── main.go
```

### (3) memcached-operatorイメージのビルドとプッシュ

Makefileにイメージを作ってPushする定義が書かれているので、それに従って実行。

```
$ make docker-build docker-push IMG="[hostname-registry]:5000/memcached-operator:v0.0.1"
```

すると、次のようなエラーが出て失敗する。


```
The push refers to repository [[hostname-registry]:5000/memcached-operator]
Get "https://[hostname-registry]:5000/v2/": http: server gave HTTP response to HTTPS client
```

どうやら、httpsのDocker Registryしか受け付けない模様。
https化するのは若干面倒なので、``/etc/docker/daemon.json``を作って以下を追記することで解決する。この設定をすれば、httpのDocker Registryとも接続できるようになる。（[参考サイト](https://qiita.com/Ikumi/items/cf5f813764810f81d5ff)）

```json:/etc/docker/daemon.json
{
 "insecure-registries": ["[hostname-registry]:5000"]
}
```

Dockerサービスの再起動を忘れずに。

```
$ sudo systemctl restart docker
```

再び以下のコマンドを実行すれば、Pushに成功するはず！

```
$ make docker-push IMG="[hostname-registry]:5000/memcached-operator:v0.0.1"
```

### (4) OLM（Operator Lifecycle Manager）のデプロイ

OLMのインストールと、memcached-operator-bundleのイメージ作成とPushは問題なくできるはず。
（２番目のコマンド「make bundle IMG=...」で対話式入力が必要になるが、requiredと言われる部分だけ何かしら文字列を入力したら問題なさそう）

```
$ operator-sdk olm install
$ make bundle IMG="[hostname-registry]:5000/memcached-operator:v0.0.1"
$ make bundle-build bundle-push BUNDLE_IMG="[hostname-registry]:5000/memcached-operator-bundle:v0.0.1"
```

以下でDocker RegistryにイメージがPushされていることを確認可能。

```
$ curl http://[hostname-registry]:5000/v2/_catalog
{"repositories":["memcached-operator","memcached-operator-bundle"]}
```

ローカルにDocker Registryを作り、かつバージョンがKubernetesのv1.24以降の場合、次の「Run your bundle」のコマンドでcontainerdにイメージをPullする際に以下のように失敗する。どうやら接続するDocker Registryがhttpだとダメな模様…。

```
$ operator-sdk run bundle [hostname-registry]:5000/memcached-operator-bundle:v0.0.1
INFO[0000] trying next host                              error="failed to do request: Head \"https://[hostname-registry]:5000/v2/memcached-operator-bundle/manifests/v0.0.1\": http: server gave HTTP response to HTTPS client" host="[hostname-registry]:5000"
FATA[0000] Failed to run bundle: pull bundle image: error pulling image [hostname-registry]:5000/memcached-operator-bundle:v0.0.1: error resolving name : failed to do request: Head "https://[hostname-registry]:5000/v2/memcached-operator-bundle/manifests/v0.0.1": http: server gave HTTP response to HTTPS client
```

そこで**クラスター内のマシン全てに対して**、containerdの設定ファイルに自作のDocker Registryとhttpでやり取りできるよう、以下のように２つの設定ファイルを作成する必要あり。（[参考サイト](https://github.com/containerd/containerd/blob/main/docs/hosts.md#bypass-tls-verification-example)）
※下記の設定はcontainerdのバージョンがv1.5以上が対象のようなので、それ以前のバージョンの場合はアップグッレードが必要

１つ目は、``/etc/containerd/config.toml``というファイル
```/etc/containerd/config.toml
version = 2

[plugins."io.containerd.grpc.v1.cri".registry]
   config_path = "/etc/containerd/certs.d"
```

２つ目は、``/etc/containerd/certs.d/[hostname-registry]:5000/hosts.toml``というファイル
```
server = "http://[hostname-registry]:5000"

[host."http://[hostname-registry]:5000"]
  capabilities = ["pull", "resolve"]
  skip_verify = true
```

ファイル編集後、containerdサービスの再起動を忘れずに。

```
$ sudo systemctl restart containerd
```

改めて「Run your bundle」のコマンドを実行。
**この時に「--use-http」のオプションをつけること！**

```
$ operator-sdk run bundle [hostname-registry]:5000/memcached-operator-bundle:v0.0.1 --use-http
```

以下のような出力が出て来て、問題なくインストールされる(はず)！

```
INFO[0026] Creating a File-Based Catalog of the bundle "[hostname-registry]:5000/memcached-operator-bundle:v0.0.1" 
INFO[0026] Generated a valid File-Based Catalog         
INFO[0029] Created registry pod: [hostname-registry]-5000-memcached-operator-bundle-v0-0-1 
INFO[0029] Created CatalogSource: memcached-catalog     
INFO[0029] Created Subscription: memcached-v0-0-1-sub   
INFO[0034] Approved InstallPlan install-4h5hn for the Subscription: memcached-v0-0-1-sub 
INFO[0034] Waiting for ClusterServiceVersion "default/memcached.v0.0.1" to reach 'Succeeded' phase 
INFO[0034]   Waiting for ClusterServiceVersion "default/memcached.v0.0.1" to appear 
INFO[0057]   Found ClusterServiceVersion "default/memcached.v0.0.1" phase: Pending 
INFO[0059]   Found ClusterServiceVersion "default/memcached.v0.0.1" phase: Installing 
INFO[0100]   Found ClusterServiceVersion "default/memcached.v0.0.1" phase: Succeeded 
INFO[0100] OLM has successfully installed "memcached.v0.0.1"
```

出来たリソースを確認してみる。

```
$ kubectl get all -n default
NAME                                                                  READY   STATUS      RESTARTS   AGE
pod/4cd7b9b9d399f3080f29723704f3c5288d8f0b632a3e194f8ca26bdae76tmfj   0/1     Completed   0          51m
pod/memcached-controller-manager-7594bd5b47-pw9bv                     2/2     Running     0          51m
pod/[hostname-registry]-5000-memcached-operator-bundle-v0-0-1         1/1     Running     0          51m

NAME                                                   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/kubernetes                                     ClusterIP   10.96.0.1       <none>        443/TCP    3d7h
service/memcached-controller-manager-metrics-service   ClusterIP   10.97.214.249   <none>        8443/TCP   51m

NAME                                           READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/memcached-controller-manager   1/1     1            1           51m

NAME                                                      DESIRED   CURRENT   READY   AGE
replicaset.apps/memcached-controller-manager-7594bd5b47   1         1         1       51m

NAME                                                                        COMPLETIONS   DURATION   AGE
job.batch/4cd7b9b9d399f3080f29723704f3c5288d8f0b632a3e194f8ca26bdae798ad0   1/1           19s        51m
```

ようやく最後に、Memcachedカスタムリソースをデプロイする。

```
$ kubectl apply -f config/samples/cache_v1alpha1_memcached.yaml
memcached.cache.raspberrypi.local/memcached-sample created
```

内容確認

```
$ kubectl get memcached -n default
NAME               AGE
memcached-sample   6m25s
```

後片付けは以下のコマンド。

```
$ operator-sdk cleanup memcached-operator
```

筆者環境だとなぜか上のコマンドでうまく消せず、次のコマンドで無理やり消した。

```
$ kubectl delete catalogsources.operators.coreos.com memcached-catalog; \
kubectl delete subscriptions.operators.coreos.com memcached-v0-0-1-sub; \
kubectl delete clusterserviceversion.operators.coreos.com memcached.v0.0.1;
```

#### (4)の別手段 :  Direct deployment

Makefileで``make deploy``すれば、olmのインストールとbundleイメージの作成は無しでデプロイ可能。
**ちょっと試すぶんにはolmを使うよりは「Direct deployment」の方が簡単そう。**

```
$ make deploy IMG="[hostname-registry]:5000/memcached-operator:v0.0.1"
```

この場合は、Namespaceはdefaultではなくてmemcached-systemにリソースが作成される。
また、olm経由でデプロイの場合にあったJobが生成されない等の違いがある。

```
$ kubectl get all -n memcached-system 
NAME                                               READY   STATUS    RESTARTS   AGE
pod/memcached-controller-manager-89f9468f6-v9lgh   2/2     Running   0          14s

NAME                                                   TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/memcached-controller-manager-metrics-service   ClusterIP   10.101.107.214   <none>        8443/TCP   14s

NAME                                           READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/memcached-controller-manager   1/1     1            1           14s

NAME                                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/memcached-controller-manager-89f9468f6   1         1         1       14s
```

Memcachedカスタムリソースはolm利用時と同じようにデプロイ可能。

```
$ kubectl apply -f config/samples/cache_v1alpha1_memcached.yaml
memcached.cache.raspberrypi.local/memcached-sample created
```

以下で後片付け。

```
$ make undeploy
```

## おわりに

この時点では空のMemcachedリソースが作られただけで、残念ながら実体はない…。ここに魂を入れてサービスが提供できる状態にするには、[full tutorial](https://sdk.operatorframework.io/docs/building-operators/golang/tutorial/)に従ってgoのソースを修正してビルドする必要あり。
[こちらのサイト](https://qiita.com/nakamasato/items/2a49d93e17893dcc3378)で実際にfull tutorialを試した内容が書かれていて参考になりそう！


## 参考サイト

https://sdk.operatorframework.io/docs/building-operators/golang/quickstart/

https://qiita.com/nakamasato/items/2a49d93e17893dcc3378

https://qiita.com/Ikumi/items/cf5f813764810f81d5ff

https://github.com/containerd/containerd/blob/main/docs/hosts.md#bypass-tls-verification-example