Red Hat OpenShift on IBM Cloudに自作Ansible Operatorをデプロイする（手動インストール編）

# 目的

Kubernetes Operatorは、KubernetesやOpenShiftクラスターのアプリケーションをパッケージングしデプロイし管理するための手段の1つです。単にデプロイするだけならHelmやKustomizeが有名ですが、Operatorのいいところはデプロイ後の管理、例えば構成の変更やバージョンアップのような、いわゆるオペレータ作業をある程度自動化することができる点です。

現在（2021/10）、Operator SDKはGo、Ansible、Helmで開発することができます。

https://sdk.operatorframework.io/

しかし私見ではありますが、Operatorの最大の課題は、非常に複雑かつ情報が少ないため、開発が困難であることだと思っています。SDKを使って公式のチュートリアルに従えばとりあえずは動きますが、ちょっとここの動きを変えたいとか、ここのネーミングを変えたいとか、何かしらの原因でうまく動いていないとか問題が起きた瞬間に途方に暮れます。そのため、単にチュートリアルに従うだけではなく、きっちり仕組みを理解しながら開発をする必要があります。

> 単にパッケージマネージャとして使うのであれば、Operatorはその労力に見合うとは個人的には思っていません。Operatorを使うメリットは、OperatorHubやRed Hat Marletplace等での公開にあると思っています。

そのため、今回は私自身の学習の兼ねて、それなりにリアルな環境でのOperatorの開発をしてみます。初心者なので間違ったことをしている可能性が高いですが、ご指摘いただければ幸いです。

ちなみに私はGoが得意ではないので、今回は使い慣れたAnsibleでOperatorを開発し、実際にIBM CloudのOpenShift上にデプロイしてみます。

# 環境

特徴としては、世の中のOperator開発のチュートリアルやサンプルは、イメージをquay.ioやdocker.io等の公開レジストリから取得していますが、今回はエンタープライズな用途を想定して、プライベートなレジストリを使用します。そのため、ImagePullSecretの考慮が常に必要になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241424/dea0621e-8289-9d51-b60a-75c4e4ec11dc.png)

# 手順

## SDKの準備

最低限、makeとoperator-sdkをインストールします。他にIBM Cloud CLI、DockerまたはPodman、OpenShift CLIも必要ですが、それらはインストール済とします。

```
$ sudo dnf -y make
$ wget https://github.com/operator-framework/operator-sdk/releases/download/v1.12.0/operator-sdk_linux_amd64
$ sudo cp operator-sdk_linux_amd64 /usr/local/bin/operator-sdk
$ sudo chmod 755 /usr/local/bin/operator-sdk
$ operator-sdk completion bash | sudo tee /etc/bash_completion.d/operator-sdk >/dev/null
$ . /etc/bash_completion.d/operator-sdk
```

## プロジェクトの作成

プロジェクトを作成します。--pluginsでAnsible Operatorであることを指定します。

```
$ mkdir hello-ansible-operator
$ cd hello-ansible-operator/

$ operator-sdk init --plugins ansible --domain teruq.example.com
Writing kustomize manifests for you to edit...
Next: define a resource with:
#$ operator-sdk create api
```

現在のプロジェクト構造を見てみます。

```
$ ls -F
Dockerfile  
Makefile  
PROJECT  
config/  
molecule/  
playbooks/  
requirements.yml  
roles/  
watches.yaml
```

特に気にすべきは2点です。configにはOperatorをデプロイするためのマニフェスト類のひな型が生成されています。rolesがOperatorの動作を定義する本体となりますが、この段階ではまだ空です。

## Operatorの開発

### APIの追加

OperatorのAPI``Hello``を追加します。それに対応したRoleのひな型が生成されます。

```
$ operator-sdk create api --group example --version v1alpha1 --kind Hello --generate-role
Writing kustomize manifests for you to edit...
```

次のようなファイルが生成されます。API``Hello``が``hello``Roleに対応します。アプリの状態管理はこれを編集していくことになります。

```
$ find roles/
roles/
roles/.placeholder
roles/hello
roles/hello/defaults
roles/hello/defaults/main.yml
roles/hello/files
roles/hello/files/.placeholder
roles/hello/handlers
roles/hello/handlers/main.yml
roles/hello/meta
roles/hello/meta/main.yml
roles/hello/README.md
roles/hello/tasks
roles/hello/tasks/main.yml
roles/hello/templates
roles/hello/templates/.placeholder
roles/hello/vars
roles/hello/vars/main.yml
```

### Roleの編集

例として、下記のように[k8sモジュール](https://docs.ansible.com/ansible/latest/collections/community/kubernetes/k8s_module.html)を使ってコンテナのDeploymentとServiceを定義します。このとき、レプリカ数を変数``replicas``にしておきます。

```roles/hello/tasks/main.yml
---
# tasks file for Hello
- name: Deployment hello is defined
  community.kubernetes.k8s:
    definition:
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: "{{ ansible_operator_meta.name }}-hello"
        namespace: "{{ ansible_operator_meta.namespace }}"
      spec:
        replicas: "{{ replicas }}"
        selector:
          matchLabels:
            app: hello
        template:
          metadata:
            labels:
              app: hello
          spec:
            containers:
            - name: hello
              image: ibmcom/hello

- name: Service hello is defined
  community.kubernetes.k8s:
    definition:
      apiVersion: v1
      kind: Service
      metadata:
        name: "{{ ansible_operator_meta.name }}-hello"
        namespace: "{{ ansible_operator_meta.namespace }}"
      spec:
        ports:
        - name: 8080-tcp
          port: 8080
          protocol: TCP
          targetPort: 8080
        selector:
          app: hello
        type: ClusterIP
```

Ansible単体でも動作するよう、``replicas``のデフォルト値をdefaultsに設定します。実際には使用しません。

```roles/hello/defaults/main.yml
---
# defaults file for Hello
replicas: 1
```

### RBACの修正

RoleでDeploymentとServiceを定義しましたが、ServiceはデフォルトではRBACで許可されていないので、次のファイルに追記します。

```config/rbac/role.yaml
 - apiGroups:
      - ""
    resources:
      - secrets
      - pods
      - pods/exec
      - pods/log
      - services  # 追加
    verbs:
      - create
      - delete
      - get
      - list
      - patch
      - update
      - watch
```

### Custom Resource Definitionの修正

Hello APIのCustom Resource Definitionを修正し、``replicas``をプロパティとして指定します。

```config/crd/bases/example.teruq.example.com_hellos.yaml
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: hellos.sample.teruq.example.com
spec:
  group: sample.teruq.example.com
  names:
    kind: Hello
    listKind: HelloList
    plural: hellos
    singular: hello
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        description: Hello is the Schema for the hellos API
        properties:
          apiVersion:
            description: 'APIVersion defines the versioned schema of this representation
              of an object. Servers should convert recognized schemas to the latest
              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conve
ntions.md#resources'
            type: string
          kind:
            description: 'Kind is a string value representing the REST resource this
              object represents. Servers may infer this from the endpoint the client
              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-con
ventions.md#types-kinds'
            type: string
          metadata:
            type: object
          spec:
            description: Spec defines the desired state of Hello
            type: object
            #x-kubernetes-preserve-unknown-fields: true                   # 削除
            properties:                                                   # 追加
              replicas:                                                   # 追加
                description: The number of replicas of Hello deployment.  # 追加
                type: integer                                             # 追加
                default: 1                                                # 追加
            required:                                                     # 追加
            - replicas                                                    # 追加
          status:
            description: Status defines the observed state of Hello
            type: object
            x-kubernetes-preserve-unknown-fields: true
        type: object
    served: true
    storage: true
    subresources:
      status: {}
```

### Makefileの修正

Makefileの中でイメージの名称やタグを指定している箇所がありますが、デフォルトのままでは使えません。今後makeする際に``make IMG=xxx``のように引数を指定することで上書きできますが、操作ミスの温床になるので、Makefileを直接修正します。

```:Makefile
# IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images.
# This variable is used to construct full image tags for bundle and catalog images.
#
# For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both
# example.com/sample-ansible-operator-bundle:$VERSION and example.com/sample-ansible-operator-catalog:$VERSION.
#IMAGE_TAG_BASE ?= teruq.example.com/hello-ansible-operator
IMAGE_TAG_BASE ?= jp.icr.io/teruq/hello-ansible-operator  # 修正

# BUNDLE_IMG defines the image:tag used for the bundle.
# You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=<some-registry>/<project-name-bundle>:<tag>)
BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION)

# Image URL to use all building/pushing image targets
#IMG ?= controller:latest
IMG ?= $(IMAGE_TAG_BASE):$(VERSION)                       # 修正
```

### Operatorイメージのビルド

Operatorのイメージを作成します。Dockerfileは自動生成されています。

```:Dockerfile
FROM quay.io/operator-framework/ansible-operator:v1.12.0

COPY requirements.yml ${HOME}/requirements.yml
RUN ansible-galaxy collection install -r ${HOME}/requirements.yml \
 && chmod -R ug+rwx ${HOME}/.ansible

COPY watches.yaml ${HOME}/watches.yaml
COPY roles/ ${HOME}/roles/
COPY playbooks/ ${HOME}/playbooks/
```

イメージをビルドします。

```
$ make docker-build
docker build -t jp.icr.io/teruq/hello-ansible-operator:0.0.1 .
...
```

### Operatorイメージのプッシュ

イメージをIBM Cloud Container Registry（ICR）にプッシュします。事前にIBM Cloud CLIで認証しておきます。

```
$ export IBMCLOUD_API_KEY=********
$ ibmcloud login
$ ibmcloud cr login
```

イメージをプッシュします。

```
$ make docker-push
docker push jp.icr.io/teruq/hello-ansible-operator:0.0.1
The push refers to repository [jp.icr.io/teruq/hello-ansible-operator]
...
0.0.1: digest: sha256:031595ea6f536daf9ae1d8b9901c4ddd69e2d7a07679e01ed189f57aef82dcd0 size: 3031
```

ICRに登録されたことを確認します。

```
$ ibmcloud cr images | grep hello-ansible
jp.icr.io/teruq/hello-ansible-operator               0.0.1                              031595ea6f53   teruq      2 minutes ago   156 MB   55 件の問題
```

### ImagePullSecretの設定

この後Operatorをデプロイしますが、そのままではイメージをICRからプルする際に権限がなく失敗します。ICRのImagePullSecretをコピーし、Operatorのデプロイ時にControllerのServiceAccountが利用できるようにします。

Operatorのネームスペースを作成します。ネームスペース名は下記のファイルで確認できます。

```config/default/kustomization.yaml
# Adds namespace to all resources.
namespace: hello-ansible-operator-system
```

ネームスペースを作成します。

```
$ oc create ns hello-ansible-operator-system
namespace/hello-ansible-operator-system created
```

defaultネームスペースからall-icr-ioシークレットをコピーします。

```
$ ibmcloud oc cluster config -c ${クラスタ}
$ oc login -u apikey -p ${APIキー}
$ oc get secret all-icr-io -n default -o yaml | grep -v namespace: | oc create -n hello-ansible-operator-system -f -
secret/all-icr-io created
```

次にOperatorのDeploymentでImagePullSecretを使用するよう変更します。また、必須ではありませんが、OperatorのImagePullPolicyをAlwaysにしておくと、Operatorイメージを同じタグでトライアンドエラーする際にイメージを取り直してくれるので便利です。

```config/manager/manager.yaml
...
      containers:
      - args:
        - --leader-elect
        - --leader-election-id=sample-ansible-operator
        image: controller:latest
        imagePullPolicy: Always  # 追加（オプション）
...
      serviceAccountName: controller-manager
      terminationGracePeriodSeconds: 10
      imagePullSecrets:
      - name: all-icr-io         # 追加
```

## Operatorのインストール

クラスタにOperatorをインストールします。

```
$ make deploy
cd config/manager && /.../hello-ansible-operator/bin/kustomize edit set image controller=jp.icr.io/teruq/hello-ansible-operator:0.0.1
/.../hello-ansible-operator/bin/kustomize build config/default | kubectl apply -f -
Warning: resource namespaces/hello-ansible-operator-system is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
namespace/hello-ansible-operator-system configured
customresourcedefinition.apiextensions.k8s.io/hellos.example.teruq.example.com created
Warning: resource serviceaccounts/hello-ansible-operator-controller-manager is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
serviceaccount/hello-ansible-operator-controller-manager configured
role.rbac.authorization.k8s.io/hello-ansible-operator-leader-election-role created
clusterrole.rbac.authorization.k8s.io/hello-ansible-operator-manager-role created
clusterrole.rbac.authorization.k8s.io/hello-ansible-operator-metrics-reader created
clusterrole.rbac.authorization.k8s.io/hello-ansible-operator-proxy-role created
rolebinding.rbac.authorization.k8s.io/hello-ansible-operator-leader-election-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/hello-ansible-operator-manager-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/hello-ansible-operator-proxy-rolebinding created
configmap/hello-ansible-operator-manager-config created
service/hello-ansible-operator-controller-manager-metrics-service created
deployment.apps/hello-ansible-operator-controller-manager created
```

Operatorが起動したことを確認します。

```
$ oc get pods -n hello-ansible-operator-system
NAME                                                         READY   STATUS    RESTARTS   AGE
hello-ansible-operator-controller-manager-55b57dd7fb-h7h4z   2/2     Running   0          51s
```

### Custom Resourceの設定

ここまでの段階でOperatorがデプロイされた状態ですが、実際にアプリはデプロイされていません。そのためにCustom Resourceを定義します。次のファイルを修正します。RoleやCustom Resource Definitionでは``replicas``のデフォルト値を1としましたが、Custom Resourceでは2とします。また、``name``も好みで変更します。

```config/samples/sample_v1alpha1_hello.yaml
apiVersion: sample.teruq.example.com/v1alpha1
kind: Hello
metadata:
  #name: hello-sample
  name: sample  # 修正
spec:
  # Add fields here
  replicas: 2   # 追加
```

## アプリのデプロイ

アプリをデプロイしたいネームスペースにCustom Resourceを作成します。

```
$ oc new-project qiita 2>/dev/null || oc project qiita
$ oc apply -f config/samples/example_v1alpha1_hello.yaml
hello.example.teruq.example.com/sample created
```

Custom Resourceが作成されたことをOperatorが自動検知して、アプリがデプロイされます。

```
$ oc get deploy | grep hello
sample-hello     2/2     2            2           27s

$ oc get pods | grep hello
sample-hello-c54fb8b58-bv4dl      1/1     Running   0          48s
sample-hello-c54fb8b58-ckfll      1/1     Running   0          48s

$ oc get svc | grep hello
sample-hello     ClusterIP   172.21.9.127    <none>        8080/TCP   66s
```

Podがなかなか起動してこない場合、Operatorからのデプロイに失敗している可能性があります。Controllerのログを確認することで何かしらヒントが得られるかもしれません。下記は正常な例です。Contorllerの中でAnsibleが動いていることがわかります。

```
$ stern -n hello-ansible-operator-system  hello-
...
hello-ansible-operator-controller-manager-55b57dd7fb-h7h4z manager {"level":"info","ts":1633185646.4900098,"logger":"proxy","msg":"Read object from cache","resource":{"IsResourceRequest":true,"Path":"/api/v1/namespaces/qiita/services/sample-hello","Verb":"get","APIPrefix":"api","APIGroup":"","APIVersion":"v1","Namespace":"qiita","Resource":"services","Subresource":"","Name":"sample-hello","Parts":["services","sample-hello"]}}
hello-ansible-operator-controller-manager-55b57dd7fb-h7h4z manager {"level":"info","ts":1633185646.8944943,"logger":"runner","msg":"Ansible-runner exited successfully","job":"7465230838818706286","name":"sample","namespace":"qiita"}
hello-ansible-operator-controller-manager-55b57dd7fb-h7h4z manager
hello-ansible-operator-controller-manager-55b57dd7fb-h7h4z manager ----- Ansible Task Status Event StdOut (example.teruq.example.com/v1alpha1, Kind=Hello, sample/qiita) -----
hello-ansible-operator-controller-manager-55b57dd7fb-h7h4z manager
hello-ansible-operator-controller-manager-55b57dd7fb-h7h4z manager
hello-ansible-operator-controller-manager-55b57dd7fb-h7h4z manager PLAY RECAP *********************************************************************
hello-ansible-operator-controller-manager-55b57dd7fb-h7h4z manager localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
hello-ansible-operator-controller-manager-55b57dd7fb-h7h4z manager
hello-ansible-operator-controller-manager-55b57dd7fb-h7h4z manager
...
```

## アプリの動作確認

Routeを作成します。

```
$ oc create route edge --service sample-hello
route.route.openshift.io/sample-hello created

$ oc get route sample-hello
NAME           HOST/PORT                                                                                                    PATH   SERVICES       PORT       TERMINATION   WILDCARD
sample-hello   sample-hello-qiita.roks-public-tok-********81bb6d6d7afea007d1a8cafd-0000.jp-tok.containers.appdomain.cloud          sample-hello   8080-tcp   edge          None
```

curlでアクセスできることを確認します。

```
$ curl https://sample-hello-qiita.roks-public-tok-********81bb6d6d7afea007d1a8cafd-0000.jp-tok.containers.appdomain.cloud
Hello World
```

## Custom Resourceの動作確認

Custom Resourceを更新してみます。``replicas``を3にします。

```
$ oc patch hello/sample --type merge -p '{"spec": {"replicas": 3}}'
hello.example.teruq.example.com/sample patched
```

Pod数が3に増えたことを確認します。

```
$ oc get pods | grep hello
sample-hello-c54fb8b58-bv4dl      1/1     Running   0          4m27s
sample-hello-c54fb8b58-ckfll      1/1     Running   0          4m27s
sample-hello-c54fb8b58-hw8fg      1/1     Running   0          16s
```

## クリーンアップ

アプリはCustom Resourceを削除すると一緒に削除されます。

```
$ oc delete -f config/samples/example_v1alpha1_hello.yaml
hello.example.teruq.example.com "sample" delete

$ oc get pods | grep hello
sample-hello-c54fb8b58-bv4dl      1/1     Terminating   0          7m43s
sample-hello-c54fb8b58-ckfll      1/1     Terminating   0          7m43s
sample-hello-c54fb8b58-hw8fg      1/1     Terminating   0          3m32s
```

Operatorを削除します。

```
$ make undeploy
/.../hello-ansible-operator/bin/kustomize build config/default | kubectl delete -f -
namespace "hello-ansible-operator-system" deleted
customresourcedefinition.apiextensions.k8s.io "hellos.example.teruq.example.com" deleted
serviceaccount "hello-ansible-operator-controller-manager" deleted
role.rbac.authorization.k8s.io "hello-ansible-operator-leader-election-role" deleted
clusterrole.rbac.authorization.k8s.io "hello-ansible-operator-manager-role" deleted
clusterrole.rbac.authorization.k8s.io "hello-ansible-operator-metrics-reader" deleted
clusterrole.rbac.authorization.k8s.io "hello-ansible-operator-proxy-role" deleted
rolebinding.rbac.authorization.k8s.io "hello-ansible-operator-leader-election-rolebinding" deleted
clusterrolebinding.rbac.authorization.k8s.io "hello-ansible-operator-manager-rolebinding" deleted
clusterrolebinding.rbac.authorization.k8s.io "hello-ansible-operator-proxy-rolebinding" deleted
configmap "hello-ansible-operator-manager-config" deleted
service "hello-ansible-operator-controller-manager-metrics-service" deleted
deployment.apps "hello-ansible-operator-controller-manager" deleted
```

ネームスペースが削除されているので、手動で追加したImagePullSecretやServiceAccountも削除されていることに注意してください。

今回の手順を完全にクリーンアップするためには、手動で作成したRouteも忘れずに。

```
$ oc delete route sample-hello -n qiita
```

# まとめ

今回はAnsible Operatorを手動でインストールしました。次回はOLM（Operator Lifecycle Manager）を使ったインストールを検証してみます。

https://qiita.com/teruq/items/34d9120ffeba75052b09