Kubernetes NativeなバックアップツールKanisterの検証

# はじめに

本検証では、Kubernetes NativeのバックアップツールKanisterの動作検証を行います。
Kanisterは、Veeam社の[Kasten(K10)](https://www.kasten.io)のベースとなっているOSSです。Kanisterは、Kubernetes上で実行されているアプリケーションのデータをオブジェクトストレージへバックアップします。
Kanisterは、アプリケーションセントリックを特徴としていることもあり、アプリケーションごとのテンプレート(Blueprint)が用意されています。Blueprintには、アプリケーションごとのバックアップの前準備、後始末などの処理が実装されているため、アプリケーションに優しいバックアップを実施することができます。
2020/12時点でBlueprintが用意されているアプリケーションは以下になります。

- Cassandra
- Couchbase
- Elasticsearch
- FoundationDB
- MongoDB
- MySQL on OpenShift using DeploymentConfig
- MySQL
- PostgreSQL with Point In Time Recovery (PITR)
- ETCD
- PostgreSQL

# 検証環境

- minikube
  - Kubernetes v1.19.4
- MinIO RELEASE.2020-12-10T01-54-29Z
- MySQL 5.7
- Kanister 0.44.0

# 事前準備

事前準備として、バックアップデータの格納先となるオブジェクトストレージ(MinIO)と、バックアップ対象のMySQLを準備します。

## MinIOのセットアップ

はじめに、MinIOのNamespace(`minio`)を作成し、移動します。

```
$ kubectl create ns minio
$ kubens minio
```

次にManifest(`minio.yaml`)を使ってMinIOをセットアップします。

- minio.yaml

```YAML
apiVersion: v1
kind: Service
metadata:
  name: minio
spec:
  ports:
  - port: 9000
    targetPort: 9000
  selector:
    app: minio
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: minio-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: minio
spec:
  serviceName: "minio"
  replicas: 1
  selector:
    matchLabels:
      app: minio
  template:
    metadata:
      labels:
        app: minio
    spec:
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: minio-pvc
      containers:
      - name: minio
        volumeMounts:
        - name: data 
          mountPath: "/data"
        image: minio/minio:RELEASE.2020-12-10T01-54-29Z
        args:
        - server
        - /data
        env:
        - name: MINIO_ACCESS_KEY
          value: "minio"
        - name: MINIO_SECRET_KEY
          value: "minio123"
        ports:
        - containerPort: 9000
          hostPort: 9000
```

minio.yamlをデプロイし、MinIOの起動を確認します。

```
$ kubectl apply -f minio.yaml

$ kubectl get svc,pod,pvc,pv
NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/minio   ClusterIP   10.106.197.201   <none>        9000/TCP   2m57s

NAME          READY   STATUS    RESTARTS   AGE
pod/minio-0   1/1     Running   0          2m57s

NAME                              STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/minio-pvc   Bound    pvc-f1016d2e-2fc5-48a0-befb-8a7304281b16   1Gi        RWO            standard       2m57s

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM             STORAGECLASS   REASON   AGE
persistentvolume/pvc-f1016d2e-2fc5-48a0-befb-8a7304281b16   1Gi        RWO            Delete           Bound    minio/minio-pvc   standard                2m57s
```

Portwardを行った後、MinIOのWebUIにアクセスしてみます。

```
$ kubectl port-forward -n minio svc/minio 9000:9000 &
```

ブラウザでURL(`http://localhost:9000`)にアクセスします。
AccessKey(`minio`), Secret Key(`mino123`)でアクセスできます。

![スクリーンショット 2020-12-12 14.19.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229880/50a0379b-8367-ef6b-c3e7-63556477d044.png)

本検証用のBuketを用意しておきます。
WebUIの右下の`+`をクリックし、`Create Bucket`を選択後、Bucket名に`kanister`と入力しBuketを作成します。

![スクリーンショット 2020-12-12 14.21.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229880/ecd3f3fc-2d49-d401-2b64-ee705e601fd7.png)

以上でMinIOの準備が完了しました。

## MySQLのセットアップ

今回はKanisterの動作検証用のMySQLのため、単一インスタンスのMySQLを作成します。
MySQLはNamespace(`default`)にセットアップします。

```
$ kubens default
```

次にManifest(`mysql.yaml`)を使ってMySQLをセットアップします。

- mysql.yaml

```YAML
apiVersion: v1
kind: Service
metadata:
  name: mysql
spec:
  ports:
  - port: 3306
  selector:
    app: mysql
  clusterIP: None
---
apiVersion: apps/v1 
kind: StatefulSet
metadata:
  name: mysql
  labels:
    app.kubernetes.io/name: mysql
    app.kubernetes.io/instance: mysql
spec:
  serviceName: "mysql"
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - image: mysql:5.7
        name: mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql
              key: mysql-root-password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 1Gi
```

MySQLのrootのパスワードをSecret(`mysql`)として作成後,
Manifest(`mysql.yaml`)をデプロイしMySQLの起動を確認します。

:memo: 今回の検証で利用するKanisterのBlueprint(`mysql-blueprint`)はLabel `app.kubernetes.io/instance:` の値をSecret名として(なぜか)利用しているため、設定する必要があります。

```
$ kubectl create secret generic mysql \
  --from-literal=mysql-root-password='password'

$ kubectl apply -f mysql.yaml 

$ kubectl get svc,pod,pvc,pv
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP    67m
service/mysql        ClusterIP   None         <none>        3306/TCP   2m26s

NAME          READY   STATUS    RESTARTS   AGE
pod/mysql-0   1/1     Running   0          2m26s

NAME                                 STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/data-mysql-0   Bound    pvc-cbbb914b-6911-477c-b77a-1adc3c849d1a   1Gi        RWO            standard       2m26s

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                  STORAGECLASS   REASON   AGE
persistentvolume/pvc-cbbb914b-6911-477c-b77a-1adc3c849d1a   1Gi        RWO            Delete           Bound    default/data-mysql-0   standard                2m26s
persistentvolume/pvc-f1016d2e-2fc5-48a0-befb-8a7304281b16   1Gi        RWO            Delete           Bound    minio/minio-pvc        standard                24m
```

MySQLにアクセスしてみます。
MySQLのrootのパスワードは`password`にmysql.yamlで設定したため、これを使い`SELECT 1`を実施しアクセスできることを確認します。

```
$ kubectl run mysql-client --image=mysql:5.7 -it --rm --restart=Never -- mysql -h mysql -uroot -ppassword -e 'SELECT 1'
mysql: [Warning] Using a password on the command line interface can be insecure.
+---+
| 1 |
+---+
| 1 |
+---+
pod "mysql-client" deleted
```

次に、本検証用に適当なテーブルを作成しレコードを追加します。

```
$ kubectl exec -ti mysql-0 -- bash
root@mysql-0:/# mysql -p    
Enter password:    # MySQLのrootのパスワードを入力
...
mysql> CREATE DATABASE test;
mysql> USE test;
mysql> CREATE TABLE pets (name VARCHAR(20), species VARCHAR(20));
mysql> INSERT INTO pets VALUES ('flare', 'dog');
mysql> INSERT INTO pets VALUES ('reno', 'dog');
mysql> SELECT * FROM pets;
+-------+---------+
| name  | species |
+-------+---------+
| flare | dog     |
| reno  | dog     |
+-------+---------+
2 rows in set (0.00 sec)

mysql> exit
root@mysql-0:/# exit
```

以上で事前準備が完了しました。

# 動作検証
## Kanisterのセットアップ

長かった前準備ですが、いよいよ本題のKanisterをセットアップします。
はじめに、KanisterのNamespace(`kanister`)を作成し、移動します。

```
$ kubectl create ns kanister
$ kubens kanister
```

次に、Helmを使ってKanister Operatorをセットアップします。

```
$ helm repo add kanister http://charts.kanister.io

$ helm install myrelease --namespace kanister kanister/kanister-operator --set image.tag=0.44.0
```

kanister operatorの起動を確認します。

```
$ $ kubectl get deploy,pod
NAME                                          READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/myrelease-kanister-operator   1/1     1            1           53s

NAME                                               READY   STATUS    RESTARTS   AGE
pod/myrelease-kanister-operator-6f97fd58f6-dqt8g   1/1     Running   0          53s
```

以上で、Kanister Operatorのセットアップが完了です。
Kanister では以下の3つのCRDもデプロイされています。

```
$ kubectl get crd |grep kanister
actionsets.cr.kanister.io   2020-12-12T05:52:57Z
blueprints.cr.kanister.io   2020-12-12T05:52:57Z
profiles.cr.kanister.io     2020-12-12T05:52:57Z
```

各CRの簡単な説明を以下に示します。

- Actionset
  - バックアップやリストアの実行するタスク(アクション)を示すリソース
- Blueprint
  - バックアップやリストアなどの処理を定義するテンプレートのリソース
- Profile
  - バックアップ先となるオブジェクトストレージのロケーション情報などを定義するリソース

Kanisterでは、上記の3つのCRを使いバックアップ/リストアを実施します。
Kanisterのワークフローを以下に示します。

![kanister workflow](https://docs.kanister.io/_images/kanister_workflow.png)

## Profileの作成

まずは、バックアップの格納先として先ほど用意したMinIOの情報を使いProfileを作成します。
ProfileのManifest(`profile.yaml`)を以下に示します。

- profile.yaml

```
apiVersion: cr.kanister.io/v1alpha1
kind: Profile
metadata:
  name: s3-profile
  namespace: kanister
location:
  type: s3Compliant
  bucket: kanister
  endpoint: http://minio.minio.svc:9000
credential:
  type: keyPair
  keyPair:
    idField: minio_access_key_id
    secretField: minio_secret_access_key
    secret:
      apiVersion: v1
      kind: Secret
      name: minio-secret
      namespace: kanister
skipSSLVerify: true
```

なお、今回作成したMinIOではTLSを設定していないため、`skipSSLVerify`は`true`としています。
profile.yamlをデプロイします。

```
$ kubectl apply -f profile.yaml
```

次に、MinIOのAccessKey, Secret KeyをもったSecret(`minio-secret`)を作成します。

```
$ kubectl create secret -n kanister generic minio-secret \
 --from-literal=minio_access_key_id='minio' \
 --from-literal=minio_secret_access_key='minio123'
```

## MySQL用のBlueprintの準備

次に、MySQLのバックアップ・リストアの処理の記載されたBlueprintのManifest(`mysql-blueprint.yaml`)をデプロイします。

```
$ git clone git@github.com:kanisterio/kanister.git

$ kubectl apply -f kanister/examples/stable/mysql/mysql-blueprint.yaml 

$ kubectl get blueprint
NAME              AGE
mysql-blueprint   11m
```

## バックアップの実行

いよいよ、バックアップを実行します。
Kanisterではバックアップ、リストアはActionsetsリソースを作成することで実行します。
バックアップのActionsetのManifest(`backup.yaml`)を示します。

```
apiVersion: cr.kanister.io/v1alpha1
kind: ActionSet
metadata:
  name: backup
  namespace: kanister
spec:
  actions:
  - blueprint: mysql-blueprint
    name: backup
    object:
      kind: statefulset
      name: mysql
      namespace: default
    profile:
      name: s3-profile
      namespace: kanister
```

バックアップが完了するまで、しばし待ちます。
バックアップのステータスは、Actionset(`backup`)のEventを見ることで確認できます。

```
$ kubectl describe actionset -n kanister backup 
...
Events:
  Type    Reason           Age    From                 Message
  ----    ------           ----   ----                 -------
  Normal  Started Action   3m18s  Kanister Controller  Executing action backup
  Normal  Started Phase    3m18s  Kanister Controller  Executing phase dumpToObjectStore
  Normal  Ended Phase      35s    Kanister Controller  Completed phase dumpToObjectStore
  Normal  Update Complete  35s    Kanister Controller  Updated ActionSet 'backup' Status->complete
``` 

`Status->complete` となりバックアップが完了しました。
MinIOのWebUIからバックアップされたデータを確認してみます。

![スクリーンショット 2020-12-12 17.33.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229880/a173169f-154e-a46e-5114-92cdde6269fe.png)

本検証用のBuket(`kanister`)配下に、ネームスペース名やStatefulSet名や日付で階層が作成され、その下に、`dump.sql.gz`というバックアップデータが格納されていました。

## リストアの実行

次にリストアを検証します。
リストアを検証するために、一旦MySQLを削除します。

```
$ kubens default
$ kubectl delete -f mysql.yaml 
$ kubectl delete pvc data-mysql-0
```

再度、MySQLのManifest(`mysql.yaml`)をデプロイします。

```
$ kubectl apply -f mysql.yaml 

$ kubectl get svc,pod,pvc,pv
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP    4h3m
service/mysql        ClusterIP   None         <none>        3306/TCP   68s

NAME          READY   STATUS    RESTARTS   AGE
pod/mysql-0   1/1     Running   0          68s

NAME                                 STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/data-mysql-0   Bound    pvc-cc959139-bf18-465d-a17c-e1727b4c1ac2   1Gi        RWO            standard       68s

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                  STORAGECLASS   REASON   AGE
persistentvolume/pvc-cc959139-bf18-465d-a17c-e1727b4c1ac2   1Gi        RWO            Delete           Bound    default/data-mysql-0   standard                68s
persistentvolume/pvc-f1016d2e-2fc5-48a0-befb-8a7304281b16   1Gi        RWO            Delete           Bound    minio/minio-pvc        standard                3h21m
```

これで、データが初期状態となったMySQLになりました。
ここに、Kanisterでバックアップしたデータをリストアしていきます。
リストアもActionsetを作り実行します。
リストア用のActionsetのManifestを作るために必要となる情報(`artifacts.mysqlCloudDump.keyValue.s3path`)を取得します。

```
$ kubectl get actionsets backup -o yaml |grep -A 3 artifacts
  - artifacts:
      mysqlCloudDump:
        keyValue:
          s3path: /mysql-backups/default/mysql/2020-12-12T08-26-08/dump.sql.gz
```

上記値を使い作成したリストア用のActionsetのManifest(`restore.yaml`)を以下に示します。

- restore.yaml

```
apiVersion: cr.kanister.io/v1alpha1
kind: ActionSet
metadata:
  name: restore
  namespace: kanister
spec:
  actions:
  - name: restore
    artifacts:
      mysqlCloudDump:
        keyValue:
          s3path: /mysql-backups/default/mysql/2020-12-12T08-26-08/dump.sql.gz
    blueprint: mysql-blueprint
    object:
      kind: statefulset
      name: mysql
      namespace: default
    profile:
      name: s3-profile
      namespace: kanister
```

Manifest(`restore.yaml`)をデプロイしリストアを実行します。
リストアが完了するまで、しばし待ちます。
リストアのステータスは、Actionset(`backup`)のEventを見ることで確認できます。

```
$ kubectl describe actionset -n kanister restore
...
Events:
  Type    Reason           Age   From                 Message
  ----    ------           ----  ----                 -------
  Normal  Started Action   85s   Kanister Controller  Executing action restore
  Normal  Started Phase    85s   Kanister Controller  Executing phase restoreFromBlobStore
  Normal  Ended Phase      83s   Kanister Controller  Completed phase restoreFromBlobStore
  Normal  Update Complete  83s   Kanister Controller  Updated ActionSet 'restore' Status->complete
``` 

`Status->complete` となり、リストアが完了しました。
MySQLに接続し、正しくリストアされているかを確認します。

```
$ kubens default

$ kubectl exec -ti mysql-0 -- bash
root@mysql-0:/# mysql -p
Enter password: 
...
mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| test               |
+--------------------+
5 rows in set (0.00 sec)

mysql> USE test
...
Database changed

mysql> SHOW TABLES;
+----------------+
| Tables_in_test |
+----------------+
| pets           |
+----------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM pets;
+-------+---------+
| name  | species |
+-------+---------+
| flare | dog     |
| reno  | dog     |
+-------+---------+
2 rows in set (0.00 sec)

mysql> exit
Bye
root@mysql-0:/# exit
```

正しくリストアできていることが確認できました。

## 補足: Actionsetを簡単にデプロイするコマンド(kanctl)

ActionsetのManifestは、利用するBlueprintをチェックしながら作成する必要があり、慣れない人には少々ハードルが高いかと思います。
Kanisterでは`kanctl`コマンドというものが用意されています。
このコマンドを使えば、バックアップやリストアをManifestを用意しなくても`kanctl`コマンドでActionsetのリソースを作成することができます。
`kanctl`コマンドは、以下でインスールできます。

```
$ curl https://raw.githubusercontent.com/kanisterio/kanister/master/scripts/get.sh | bash
```

例えば、上記検証のリストアはActionsetのMainfestを作成しなくても以下の`kanctl`コマンドでも同様に実行できます。

```
$ kanctl --namespace kanister create actionset --action restore --from backup
```

# 感想
今回は、Kubernetes Nativeなバックアップ/リストアのKanisterの動作検証を行いました。Kanisterは、アプリケーションごとにBlueprintというテンプレートを用意することで、アプリケーションごとのやり方でバックアップ/リストアを行うというアプローチをとっているツールでした。Blueprintが既に用意されているアプリケーションであれば、簡単に利用することができます。ただし、Blueprintが用意されているからといって、過信することは危険です。利用する前には必ずBlueprintの中身をチェックし、どのような処理が行われているのを確認することをお勧めします。
例えば、今回利用したBlueprint(`mysql-blueprint`)の中身は`mysqldump`コマンドで取得したバックアップファイルをオブジェクトストレージへコピーしているだけのシンプルなものです。バックアップ中のレコードが書き換えられないように、書き込みをロックするなどDBのバックアップでセオリーとなるような処理は残念ながら入っていません。必要な人は、Blueprintに自分で追加する必要があります。
Kanisterは、GitHubのページを見ると`A framework for data management in Kubernetes.`と記載されています。つまり、アプリケーションセントリックなバックアップ/リストアをするための、**フレームワーク**との位置付けのようです。バックアップ/リストア用のソフトとして提供しているKasten(K10)との差別化なのかもしれません。

また、似たようなKubernetes Nativeなバックアップ/リストアのツールとしてVeleroがあります。(Veleroの検証レポートは[こちら](https://qiita.com/ysakashita/items/3a0b2ad9ac37e2ce315a))
VeleroとKanisterの違いが気になる人もいるかと思います。一言で違いを述べると、VeleroはKubernetesにデプロイしたリソース(Pod, PVC,PVなど)を一式まるごとバックアップ/リストアするのに優れています。一方、Kanisterはアプリケーションのデータのみをアプリケーションに適したやり方でバックアップ/リストアを行います。
Kubernetesのリソース丸ごとバックアップするのか、それとも特定のアプリケーションのデータのみバックアップするのかなど利用シーンにあわせて、選択するのをお勧めします。

# 参考情報

- [GitHub kanisterio/kanister](https://github.com/kanisterio/kanister)
- [Kanister docs](https://docs.kanister.io)