MySQL OperatorをOpenShiftで使ってみた


# はじめに
MySQL Operator for Kubernetesは、MySQLをKubernetes上で稼働させるために各種存在するOperatorの1つで、Oracleにより開発されています。MySQL 8.0.29のリリースと共に、MySQL Operator for KubernetesがGAとなったことが発表されました。

https://codezine.jp/article/detail/15912

MySQL Operator for Kubernetesを使用すると、InnoDBコンテナを複数個起動しInnoDBクラスターを構成すると共に、アプリケーションとInnoDBクラスタを接続するMySQL Routerもコンテナとして稼働させて、全体で高可用性環境を実現します。この環境をKubernetesのコンテナ管理とOperatorの機能を用いることで、環境の自動的なプロビジョニングが可能となります。

https://dev.mysql.com/doc/refman/8.0/ja/mysql-innodb-cluster-introduction.html

MySQL Operatorのインストール方法は、[MySQL Operator GitHub Project Page](https://github.com/mysql/mysql-operator)のREADME.mdで説明されていますが、この公式ガイドそのままではOpenShiftでPodとして起動できないため、ここではOpenShiftにMySQL Operatorをインストールする追加の手順を含めて説明します。

# 前提
OpenShiftは、CodeReady Containersの2.4.1（Linux版）を使用します。これはOpenShift4.10.14に相当しますが、MySQL Operatorには前提とするKubernetesバージョンやOpenShiftバージョンは指定されていないので、より古いバージョンでも手順は同じかと思います。

```
$ crc version
CRC version: 2.4.1+b877358
OpenShift version: 4.10.14
Podman version: 4.0.2
$ oc get clusterversion
NAME      VERSION   AVAILABLE   PROGRESSING   SINCE   STATUS
version   4.10.14   True        False         20d     Cluster version is 4.10.14
```

# インストール
MySQL Operatorのインストールは、Operator自身のインストールとInnoDBクラスターのインストールの2段階で行います。

また公式ガイドでは、インストール方法としてHelmを使用する方法と、kubectlコマンドを使用する方法それぞれ説明されていますが、ここではkubectl（正確にはoc）コマンドを使用した方法に沿って説明します。

MySQL OperatorとInnoDBクラスターは、それぞれ異なるProjectにインストールすることとします。MySQL Operator用に`mysql-operator`、InnoDBクラスター用に`mysql`Projectを使用します。インストール手順の中で使用する`deploy-operator.yaml`ファイルには、MySQL Operatorが使用するnamespaceとして`mysql-operator`が直書きされているので、そのまま使用します。

## Operatorインストール

### CRDインストール
最初にMySQL Operatorが使用するCRD（Custom Resource Definition）をインストールします。公式ガイドのコマンドパラメータをそのまま使用します。

```
$ oc apply -f https://raw.githubusercontent.com/mysql/mysql-operator/trunk/deploy/deploy-crds.yaml
customresourcedefinition.apiextensions.k8s.io/innodbclusters.mysql.oracle.com created
customresourcedefinition.apiextensions.k8s.io/mysqlbackups.mysql.oracle.com created
customresourcedefinition.apiextensions.k8s.io/clusterkopfpeerings.zalando.org created
customresourcedefinition.apiextensions.k8s.io/kopfpeerings.zalando.org created
```

### MySQL Operator用Projectの作成
公式ガイドが提供する`deploy-operator.yaml`には、Operatorの使用するClusterRoleとServiceAccountおよびそのロールへのClusterRoleBinding、そしてDeploymentとNamespaceが定義されています。Projectは作成してくれないので、このyamlをapplyする前に`mysql-operator` Projectを作成します。

```
$ oc new-project mysql-operator
Now using project "mysql-operator" on server "https://api.crc.testing:6443".

You can add applications to this project with the 'new-app' command. For example, try:

    oc new-app rails-postgresql-example

to build a new example application in Ruby. Or use kubectl to deploy a simple Kubernetes application:

    kubectl create deployment hello-node --image=k8s.gcr.io/e2e-test-images/agnhost:2.33 -- /agnhost serve-hostname
```


### MySQL Operatorインストール

この後、手順の通り`deploy-operator.yaml`をapplyします。


```
$ oc apply -f https://raw.githubusercontent.com/mysql/mysql-operator/trunk/deploy/deploy-operator.yaml
clusterrole.rbac.authorization.k8s.io/mysql-operator created
clusterrole.rbac.authorization.k8s.io/mysql-sidecar created
clusterrolebinding.rbac.authorization.k8s.io/mysql-operator-rolebinding created
clusterkopfpeering.zalando.org/mysql-operator created
Warning: resource namespaces/mysql-operator is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by oc apply. oc apply should only be used on resources created declaratively by either oc create --save-config or oc apply. The missing annotation will be patched automatically.
namespace/mysql-operator configured
serviceaccount/mysql-operator-sa created
deployment.apps/mysql-operator created
```

先にProjectを作成済みであるため、NamespaceのところでWarningが出ていますが、気にせず進みます。これで、mysql-operator Deploymentが作成されたので、operator podが立ち上がるはずですが、いつまでたってもPodが起動する気配がありません。何が起きているかを知るためにEventを見てみましょう。

```
$ oc get event
LAST SEEN   TYPE      REASON              OBJECT                                 MESSAGE
7s          Warning   FailedCreate        replicaset/mysql-operator-586f9f5d5b   Error creating: pods "mysql-operator-586f9f5d5b-" is forbidden: unable to validate against any security context constraint: [provider "anyuid": Forbidden: not usable by user or serviceaccount, spec.containers[0].securityContext.runAsUser: Invalid value: 2: must be in the ranges: [1000640000, 1000649999], provider "nonroot": Forbidden: not usable by user or serviceaccount, provider "hostmount-anyuid": Forbidden: not usable by user or serviceaccount, provider "machine-api-termination-handler": Forbidden: not usable by user or serviceaccount, provider "hostnetwork": Forbidden: not usable by user or serviceaccount, provider "hostaccess": Forbidden: not usable by user or serviceaccount, provider "privileged": Forbidden: not usable by user or serviceaccount]
2m50s       Normal    ScalingReplicaSet   deployment/mysql-operator              Scaled up replica set mysql-operator-586f9f5d5b to 1
```

Operator Podが使用するServiceAccountに適切なSCCが設定されていないため、Podの作成でFailedCreateになっていました。これを解消するために、Podが使用するServiceAccountにSCCを付与します。各種SCCについてのエラーが出ていますが、ここではanyuidのみ設定すれば問題は解消します。

```
$ oc get serviceaccount
NAME                SECRETS   AGE
builder             2         9m30s
default             2         9m30s
deployer            2         9m30s
mysql-operator-sa   2         8m7s
$ oc get deployment mysql-operator -o custom-columns=NAME:metadata.name,ACCOUNT:spec.template.spec.serviceAccountName
NAME             ACCOUNT
mysql-operator   mysql-operator-sa
$ oc adm policy add-scc-to-user anyuid -z mysql-operator-sa
clusterrole.rbac.authorization.k8s.io/system:openshift:scc:anyuid added: "mysql-operator-sa"
```

しばらく待っていると、Podの作成が成功して、Operator Podが起動します。
```
$ oc get event
LAST SEEN   TYPE      REASON              OBJECT                                 MESSAGE
21s         Normal    Scheduled           pod/mysql-operator-586f9f5d5b-hsqck    Successfully assigned mysql-operator/mysql-operator-586f9f5d5b-hsqck to crc-m5qn9-master-0
19s         Normal    AddedInterface      pod/mysql-operator-586f9f5d5b-hsqck    Add eth0 [10.217.0.131/23] from openshift-sdn
19s         Normal    Pulling             pod/mysql-operator-586f9f5d5b-hsqck    Pulling image "mysql/mysql-operator:8.0.29-2.0.4"
7m57s       Warning   FailedCreate        replicaset/mysql-operator-586f9f5d5b   Error creating: pods "mysql-operator-586f9f5d5b-" is forbidden: unable to validate against any security context constraint: [provider "anyuid": Forbidden: not usable by user or serviceaccount, spec.containers[0].securityContext.runAsUser: Invalid value: 2: must be in the ranges: [1000640000, 1000649999], provider "nonroot": Forbidden: not usable by user or serviceaccount, provider "hostmount-anyuid": Forbidden: not usable by user or serviceaccount, provider "machine-api-termination-handler": Forbidden: not usable by user or serviceaccount, provider "hostnetwork": Forbidden: not usable by user or serviceaccount, provider "hostaccess": Forbidden: not usable by user or serviceaccount, provider "privileged": Forbidden: not usable by user or serviceaccount]
21s         Normal    SuccessfulCreate    replicaset/mysql-operator-586f9f5d5b   Created pod: mysql-operator-586f9f5d5b-hsqck
13m         Normal    ScalingReplicaSet   deployment/mysql-operator              Scaled up replica set mysql-operator-586f9f5d5b to 1
$ oc get pod
NAME                              READY   STATUS    RESTARTS   AGE
mysql-operator-586f9f5d5b-hsqck   1/1     Running   0          82s
```

## InnoDBクラスターインストール
Operatorが立ち上がったので、続いてInnoDBクラスターをインストールします。ここでは公式ガイドに従い、3個のInnoDB Podと1個のMySQL Router Podを起動します。

### クラスター用Projectの作成
InnoDBクラスターは、mysql Projectに作成することにしたので、ここでそのProjectを作成します。この後、管理者パスワード用のSecretや、InnoDBClusterリソースをこのProjectに作成します。

```
$ oc new-project mysql
Now using project "mysql" on server "https://api.crc.testing:6443".

You can add applications to this project with the 'new-app' command. For example, try:

    oc new-app rails-postgresql-example

to build a new example application in Ruby. Or use kubectl to deploy a simple Kubernetes application:

    kubectl create deployment hello-node --image=k8s.gcr.io/e2e-test-images/agnhost:2.33 -- /agnhost serve-hostname
```


### Secretの作成
まず、管理者用のパスワードをSecretに指定します。ガイドの通り管理ユーザをroot、パスワードをsakilaとしていますが、少なくともパスワードは変更すべきです。

```
$ oc create secret generic mypwds --from-literal=rootUser=root --from-literal=rootHost=% --from-literal=rootPassword="sakila"
secret/mypwds created
```

### クラスターPodとrouter Podのインストール
InnoDBクラスターPodは、InnoDBClusterリソースの作成によりインストールできます。yaml定義は公式ガイドで提供されているものをそのまま使用し、InnoDB Pod3個、MySQL Router Pod1個含み、管理ユーザを先ほど作成したSecretの内容とした`mycluster`クラスターが作成されます。

ガイドでは一旦ファイル`mycluster.yaml`に保存した後、そのファイルを`oc apply`コマンドに渡していますが、ここでは標準入力を使います。-fオプションのパラメータとしてハイフン '-'を指定することで、入力は標準入力となります。ガイドのyamlをコピー&ペーストして、Ctrl-Dで入力を終了します。

```
$ oc apply -f -
apiVersion: mysql.oracle.com/v2
kind: InnoDBCluster
metadata:
  name: mycluster
spec:
  secretName: mypwds
  tlsUseSelfSigned: true
  instances: 3
  router:
    instances: 1
^D
innodbcluster.mysql.oracle.com/mycluster created
```

これで、InnoDB PodのStatefulSet。MySQL RouterのDeploymentとReplicaSetおよびそれらのServiceが作成されますが、ここでもPodが立ち上がる気配はありません。MySQL Operatorのインストールと同様にEventを確認します。

```
$ oc get events --field-selector "type!=Normal"
LAST SEEN   TYPE      REASON         OBJECT                  MESSAGE
1s          Warning   FailedCreate   statefulset/mycluster   create Pod mycluster-0 in StatefulSet mycluster failed error: pods "mycluster-0" is forbidden: unable to validate against any security context constraint: [spec.initContainers[2].securityContext.capabilities.add: Invalid value: "DAC_OVERRIDE": capability may not be added, spec.initContainers[2].securityContext.capabilities.add: Invalid value: "SETGID": capability may not be added, spec.initContainers[2].securityContext.capabilities.add: Invalid value: "SETUID": capability may not be added, spec.initContainers[2].securityContext.capabilities.add: Invalid value: "SYS_NICE": capability may not be added, spec.initContainers[2].securityContext.capabilities.add: Invalid value: "SYS_RESOURCE": capability may not be added, spec.containers[1].securityContext.capabilities.add: Invalid value: "DAC_OVERRIDE": capability may not be added, spec.containers[1].securityContext.capabilities.add: Invalid value: "SETGID": capability may not be added, spec.containers[1].securityContext.capabilities.add: Invalid value: "SETUID": capability may not be added, spec.containers[1].securityContext.capabilities.add: Invalid value: "SYS_NICE": capability may not be added, spec.containers[1].securityContext.capabilities.add: Invalid value: "SYS_RESOURCE": capability may not be added, provider restricted: .spec.securityContext.fsGroup: Invalid value: []int64{27}: 27 is not an allowed group, spec.initContainers[0].securityContext.runAsUser: Invalid value: 0: must be in the ranges: [1000680000, 1000689999], spec.initContainers[1].securityContext.runAsUser: Invalid value: 27: must be in the ranges: [1000680000, 1000689999], spec.initContainers[2].securityContext.runAsUser: Invalid value: 27: must be in the ranges: [1000680000, 1000689999], spec.containers[0].securityContext.runAsUser: Invalid value: 27: must be in the ranges: [1000680000, 1000689999], spec.containers[1].securityContext.runAsUser: Invalid value: 27: must be in the ranges: [1000680000, 1000689999], provider "nonroot": Forbidden: not usable by user or serviceaccount, provider "hostmount-anyuid": Forbidden: not usable by user or serviceaccount, provider "machine-api-termination-handler": Forbidden: not usable by user or serviceaccount, provider "hostnetwork": Forbidden: not usable by user or serviceaccount, provider "hostaccess": Forbidden: not usable by user or serviceaccount, provider "privileged": Forbidden: not usable by user or serviceaccount]
```

Operatorと同様にSCCの問題がStatefulSet `mycluster`にあることがわかります。使用されているServiceAccountが存在することを確認し、SCCを付与します。ここでは、privileged SCCが必要となります。

```
$ oc get serviceaccount
NAME                   SECRETS   AGE
builder                2         25m
default                2         25m
deployer               2         25m
mycluster-sidecar-sa   2         21m
$ oc get statefulset mycluster -o custom-columns=NAME:metadata.name,ACCOUNT:spec.template.spec.serviceAccountName
NAME        ACCOUNT
mycluster   mycluster-sidecar-sa
$ oc adm policy add-scc-to-user privileged -z mycluster-sidecar-sa
clusterrole.rbac.authorization.k8s.io/system:openshift:scc:anyuid added: "mycluster-sidecar-sa"
```

これで、Podが起動します。

:::note info
InnoDB Podは、デフォルトで100GBのRWO PVを使用します。つまり100GBのPVが合計3個必要となります。ここで使用しているCodeReady Containersでは、開発用途のPVがあらかじめ用意されているため特に手当は必要ありませんが、実際の環境では別途ストレージが必要となります。Podが使用するPVCがPVとbindできないとPodはPendingのままとなります。

```
$ oc get pvc
NAME                  STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
datadir-mycluster-0   Bound    pv0019   100Gi      RWO,ROX,RWX                   22m
datadir-mycluster-1   Bound    pv0012   100Gi      RWO,ROX,RWX                   21m
datadir-mycluster-2   Bound    pv0010   100Gi      RWO,ROX,RWX                   21m
```
:::

最初は、InnoDB Podが3個。これが正常に起動した後、MySQL RouterのPodが1個上がります。InnoDB Podは、READINESS GATEを持っているので、これが`2/2`になると正常に稼働していることがわかります。その確認のためには、`oc get pod`コマンドに`-o wide`オプションが必要です。

```
$ oc get pod -o wide
NAME                                READY   STATUS    RESTARTS   AGE   IP             NODE                 NOMINATED NODE   READINESS GATES
mycluster-0                         2/2     Running   0          99m   10.217.0.165   crc-m5qn9-master-0   <none>           2/2
mycluster-1                         2/2     Running   0          99m   10.217.0.166   crc-m5qn9-master-0   <none>           2/2
mycluster-2                         2/2     Running   0          99m   10.217.0.167   crc-m5qn9-master-0   <none>           2/2
mycluster-router-8498dd4468-6lx6m   1/1     Running   0          97m   10.217.0.169   crc-m5qn9-master-0   <none>           <none>
$ oc get statefulset mycluster -o wide
NAME        READY   AGE    CONTAINERS      IMAGES
mycluster   3/3     100m   sidecar,mysql   mysql/mysql-operator:8.0.29-2.0.4,mysql/mysql-server:8.0.29
```

これで、MySQL OperatorおよびOperatorが制御するInnoDBクラスターがインストールできました。


# 稼働確認
クラスターが立ち上がったので、MySQLが機能していることを確認しましょう。

## クラスターの状態
インストールの中で、InnoDB Podが3個`RUNNING`であり、`READY=2/2`かつ`READINESS GATE=2/2`となりました。Podとしての正常性はこれで確認できるので、MySQL内部の状態を調べます。

クラスターの状態確認は、mysqlshコマンドを使用します。公式のガイドでは、mysqlsh用のPodを個別に立ち上げています。ガイドの中にも記載されている通りMySQLコンテナの中にもmysqlshはインストールされているので、ここではそれを使用しましょう。

InnoDB Podでmysqlshを実行して、クラスターの`status()`出力を確認します。InnoDB Podには、MySQLのコンテナとsidecarコンテナが稼働しているため、MySQLコンテナを指定する必要がある点に注意してください。まず、mycluster-0 Podのmysqlコンテナでmysqlshを実行します。

```
$ oc get pod
NAME                                READY   STATUS    RESTARTS   AGE
mycluster-0                         2/2     Running   0          32m
mycluster-1                         2/2     Running   0          32m
mycluster-2                         2/2     Running   0          32m
mycluster-router-8498dd4468-6lx6m   1/1     Running   0          29m
$ oc exec -it mycluster-0 -c mysql -- mysqlsh
Cannot set LC_ALL to locale en_US.UTF-8: No such file or directory
MySQL Shell 8.0.29

Copyright (c) 2016, 2022, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its affiliates.
Other names may be trademarks of their respective owners.

Type '\help' or '\?' for help; '\quit' to exit.
 MySQL  JS >
```

クラスターに管理者IDで接続し、`dba.getCluster().status()`を実行します。ユーザrootのパスワードは、InnoDBクラスターのインストールで作成したSecretに記載したものを使用します。

```
 MySQL  JS > \c root@mycluster
Creating a session to 'root@mycluster'
Please provide the password for 'root@mycluster': ******
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 4555 (X protocol)
Server version: 8.0.29 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.
 MySQL  mycluster:33060+ ssl  JS > dba.getCluster().status()
{
    "clusterName": "mycluster", 
    "defaultReplicaSet": {
        "name": "default", 
        "primary": "mycluster-0.mycluster-instances.mysql.svc.cluster.local:3306", 
        "ssl": "REQUIRED", 
        "status": "OK", 
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.", 
        "topology": {
            "mycluster-0.mycluster-instances.mysql.svc.cluster.local:3306": {
                "address": "mycluster-0.mycluster-instances.mysql.svc.cluster.local:3306", 
                "memberRole": "PRIMARY", 
                "mode": "R/W", 
                "readReplicas": {}, 
                "replicationLag": null, 
                "role": "HA", 
                "status": "ONLINE", 
                "version": "8.0.29"
            }, 
            "mycluster-1.mycluster-instances.mysql.svc.cluster.local:3306": {
                "address": "mycluster-1.mycluster-instances.mysql.svc.cluster.local:3306", 
                "memberRole": "SECONDARY", 
                "mode": "R/O", 
                "readReplicas": {}, 
                "replicationLag": null, 
                "role": "HA", 
                "status": "ONLINE", 
                "version": "8.0.29"
            }, 
            "mycluster-2.mycluster-instances.mysql.svc.cluster.local:3306": {
                "address": "mycluster-2.mycluster-instances.mysql.svc.cluster.local:3306", 
                "memberRole": "SECONDARY", 
                "mode": "R/O", 
                "readReplicas": {}, 
                "replicationLag": null, 
                "role": "HA", 
                "status": "ONLINE", 
                "version": "8.0.29"
            }
        }, 
        "topologyMode": "Single-Primary"
    }, 
    "groupInformationSourceMember": "mycluster-0.mycluster-instances.mysql.svc.cluster.local:3306"
}
 MySQL  mycluster:33060+ ssl  JS > \q
Bye!
bash-4.4$ exit
exit
```

クラスターの中では、各InnoDB Podはインスタンスと認識されています。StatefulSetであるPodは、pod DNSを介して`<pod名>.<headless service名>.<namespace名>.svc.cluster.local`として識別されます。

この出力から、
* クラスター全体のstatusがOKであること
* クラスターには3つのインスタンスが登録され、それぞれONLINEであること
* mycluster-0 PodがPRIMARYロールを持ちR/Wモードで稼働していること
* それ以外のPodはSECONDARYロールを持ちR/Oモードで稼働していること

などがわかります。

## MySQL DBへのアクセス
Podの状態とmysqlshを使用した確認では、外形的にMySQLが正常動作しているように見えました。ここでは実際にDBへのアクセスを行い、DBとしての機能が正常であることを確認しましょう。

ガイドでは、3306ポートをforwardして、OpenShiftの外部からDBにアクセスする方法が紹介されていましたが、ここではMySQL Router Podのmysqlコマンドを使用します。


```
$ oc get pod
NAME                                READY   STATUS    RESTARTS   AGE
mycluster-0                         2/2     Running   0          52m
mycluster-1                         2/2     Running   0          52m
mycluster-2                         2/2     Running   0          52m
mycluster-router-8498dd4468-6lx6m   1/1     Running   0          49m
$ oc exec -it mycluster-router-8498dd4468-6lx6m -- /bin/bash
bash-4.4$ mysql -uroot -p
Enter password: ******
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 15077
Server version: 8.0.29 MySQL Community Server - GPL

Copyright (c) 2000, 2022, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>
```

performance_schema DBでは、InnoDBクラスター関連のデータを管理しているので、そこからインスタンスメンバーについて調べてみましょう。

```
mysql> show databases;
+-------------------------------+
| Database                      |
+-------------------------------+
| information_schema            |
| mysql                         |
| mysql_innodb_cluster_metadata |
| performance_schema            |
| sys                           |
+-------------------------------+
5 rows in set (0.00 sec)

mysql> use performance_schema;  
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select MEMBER_HOST,MEMBER_STATE,MEMBER_ROLE from replication_group_members;
+---------------------------------------------------------+--------------+-------------+
| MEMBER_HOST                                             | MEMBER_STATE | MEMBER_ROLE |
+---------------------------------------------------------+--------------+-------------+
| mycluster-1.mycluster-instances.mysql.svc.cluster.local | ONLINE       | SECONDARY   |
| mycluster-0.mycluster-instances.mysql.svc.cluster.local | ONLINE       | PRIMARY     |
| mycluster-2.mycluster-instances.mysql.svc.cluster.local | ONLINE       | SECONDARY   |
+---------------------------------------------------------+--------------+-------------+
3 rows in set (0.00 sec)

mysql> quit
Bye
```

DBの内容が表示され、DBが正常に機能していることがわかりました。


# おわりに
MySQLの高可用性オプションとして提供されているInnoDBクラスターをOpenShift上に構築し、健全性の確認まで行いました。コンテナを使用することで、複数のInnoDBインスタンスの作成が一度に完了し、Operatorで自動管理されます。ここでは取り上げませんでしたが、InnoDB Podの配置を適切にノード上に分散させれば、仮想サーバで組んだInnoDBクラスターと同等の耐障害性も得られます。

Kubernetes上では主にstatelessなアプリケーションを中心に稼働が進んでいますが、こうしたコンテナ稼働を意識したDatabaseが普及して、statefulなアプリケーションでも利用が広がることを期待しています。