Kubernetes operator "Kubegres"で、PostgreSQLのクラスタ化を検証

最近(2021年4月)Kubegresがリリースされたので使ってみました。本記事では、minikube上でKubegresをインストールして設定する手順をまとめました。

Kubegresはコンテナ管理を自動化するKubernetesのOperatorのひとつで、PostgreSQLのHAクラスタを構築できるものです。また、minikubeはローカルマシン上に簡単にKubernetesクラスタを構築できるツールです。

これらを使ってPostgreSQLを冗長化してみます。Podの構成は以下の図のようになっています。
![PostgreSQLサーバ構成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581362/3ca4ae57-02dc-c594-bb92-eae075ff9bb5.png)

PostgreSQLは、まずは1台のPRIMARYと2台のREPLICAで構成し、あとでREPLICAを1台追加します。アプリケーションを想定したPodを用意し、そこからPostgreSQLにアクセスしてみます。
また、自動フェイルオーバー、Dockerイメージのアップデートの確認も行いました。

今回使用したツールのバージョンは以下のとおりです。

* kubegres v1.1 (postgres 13.2)
* minikube v1.18.1
* kubectl v1.20
* Docker v20.10.6

# minikubeのインストールと起動
1. 必要なパッケージをインストールします。

    ```shell
    $ sudo apt install conntrack
    ```
1. minikube本体を[公式サイトの手順](https://minikube.sigs.k8s.io/docs/start/)に従ってインストールします。例えば、

    ```shell
    $ curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
    $ sudo install minikube-linux-amd64 /usr/local/bin/minikube
    ```

1. minikube用にシステムのプロキシを設定します（[参考](https://minikube.sigs.k8s.io/docs/handbook/vpn_and_proxy/)）。プロキシを使わない場合はスキップしてください。
   * ```10.96.0.0/12,192.168.99.0/24,192.168.39.0/24``` をNO_PROXYに設定する。
   * HTTPプロキシだけでなく、HTTPS_PROXYの設定をお忘れなく。
1. 起動します。

    ```shell
    $ minikube start --driver=none
    ```
1. 確認します。以下のように表示されればOKです。

    ```shell
    $ minikube profile list
    |----------|-----------|---------|-----------|------|---------|---------|-------|
    | Profile  | VM Driver | Runtime |    IP     | Port | Version | Status  | Nodes |
    |----------|-----------|---------|-----------|------|---------|---------|-------|
    | minikube | none      | docker  | 10.0.2.15 | 8443 | v1.20.2 | Running |     1 |
    |----------|-----------|---------|-----------|------|---------|---------|-------|
    ```

# kubectlのインストール
1. [公式サイトの手順](https://kubernetes.io/ja/docs/tasks/tools/install-kubectl/)に従ってインストールします。バイナリをダウンロードし、パスの通ったところに配置します。

    ```shell
    $ curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl"
    $ chmod +x ./kubectl
    $ sudo mv ./kubectl /usr/local/bin/kubectl
    ```
1. 確認します。以下のようにバージョンが表示されればOKです。

    ```shell
    $ kubectl version --client
    Client Version: version.Info{Major:"1", Minor:"20", GitVersion:"v1.20.5", GitCommit:"6b1d87acf3c8253c123756b9e61dac642678305f", GitTreeState:"clean", BuildDate:"2021-03-18T01:10:43Z", GoVersion:"go1.15.8", Compiler:"gc", Platform:"linux/amd64"}
    ```

# Kubegresのインストール
こちらも[公式サイト](https://www.kubegres.io/doc/getting-started.html)の手順に従ってインストールします。

## Operatorのインストール
1. 以下のコマンドを実行します。

    ```shell
    $ kubectl apply -f https://raw.githubusercontent.com/reactive-tech/kubegres/v1.1/kubegres.yaml
    ```
1. 確認します。以下のように4つのリソースが表示されればOKです。

    ```shell
    $ kubectl get all -n kubegres-system
    NAME                                               READY   STATUS    RESTARTS   AGE
    pod/kubegres-controller-manager-5b6797895c-ph9gj   2/2     Running   0          4m48s

    NAME                                                  TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
    service/kubegres-controller-manager-metrics-service   ClusterIP   10.99.241.122   <none>        8443/TCP   4m48s

    NAME                                          READY   UP-TO-DATE   AVAILABLE   AGE
    deployment.apps/kubegres-controller-manager   1/1     1            1           4m48s

    NAME                                                     DESIRED   CURRENT   READY   AGE
    replicaset.apps/kubegres-controller-manager-5b6797895c   1         1         1       4m48s
    ```

## PostgreSQLの管理者ユーザの認証情報の設定と適用
1. `my-postgres-secret.yaml`ファイルを作成し、以下の内容を記述します。

    ```yaml
    apiVersion: v1
    kind: Secret
    metadata:
      name: mypostgres-secret
      namespace: default
    type: Opaque
    stringData:
      superUserPassword: postgresSuperUserPsw
      replicationUserPassword: postgresReplicaPsw
    ```
   `superUserPassword`で指定する部分がpostgresユーザのパスワード、`replicationUserPassword`で指定する部分がレプリケーション用ユーザのパスワードです。
1. ファイルが作成できたら、以下のコマンドで適用します。

    ```shell
    $ kubectl apply -f my-postgres-secret.yaml
    ```

## PostgreSQLのサーバ構成の設定と適用
1. `my-postgres.yaml`ファイルを作成し、以下の内容を記述します。  

    ```yaml
    apiVersion: kubegres.reactive-tech.io/v1
    kind: Kubegres
    metadata:
      name: mypostgres
      namespace: default
    spec:
       replicas: 3
       image: postgres:13.2
       database:
          size: 200Mi
          storageClassName: standard
       env:
          - name: POSTGRES_PASSWORD
            valueFrom:
               secretKeyRef:
                  name: mypostgres-secret
                  key: superUserPassword
          - name: POSTGRES_REPLICATION_PASSWORD
            valueFrom:
               secretKeyRef:
                  name: mypostgres-secret
                  key: replicationUserPassword
    ```

    `replicas: 3`として、3台のPostgreSQLを起動するように指定します。
2. ファイルが作成できたら、以下のコマンドで適用します。

    ```shell
    $ kubectl apply -f my-postgres.yaml
    ```

これで、PostgreSQLが自動で立ち上がり、レプリケーションが開始されます。

# 動作確認
動作確認のために、PostgreSQLにアクセスするためのPodを別途用意しました。
そのPodからplsqを使いPRIMARY/REPLICAに接続してみます。

## plsq用のPodの作成
1. 今回、psqlが使えるPodを用意するために、postgres:13.2のイメージを使いました。  
以下の内容を`psql.yaml`ファイルに記述します。

    ```yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: psql-pod
      namespace: default  
    spec:
      containers:
        - name: psql
          image: postgres:13.2
          env:
          - name: POSTGRES_PASSWORD
            value: pgpwd
    ```
1. ファイルが作成できたら、以下のコマンドで適用します。

    ```shell
    $ kubectl apply -f psql.yaml
    ```

## PostgreSQLへのアクセス

* まず以下のコマンドを実行してPodの状態を確認します。3台のPodのステータスがRunningになっていればOKです。

   ```shell
   $ kubectl get pods -o wide
   NAME             READY   STATUS    RESTARTS   AGE     IP           NODE       NOMINATED NODE   READINESS GATES
   mypostgres-1-0   1/1     Running   0          3m      172.17.0.6   hostname   <none>           <none>
   mypostgres-2-0   1/1     Running   0          2m46s   172.17.0.7   hostname   <none>           <none>
   mypostgres-3-0   1/1     Running   0          2m34s   172.17.0.8   hostname   <none>           <none>
   ```
* PRIMARYに接続してみます。
    PRIMARYには`mypostgres`というホスト名でアクセスできます。
    以下のコマンドでPostgreSQLに接続します。

    ```shell
    $ kubectl exec -it psql-pod -- psql -h mypostgres -U postgres
    ```
   ここでパスワードを聞かれるので、`my-postgres-secret.yaml`ファイルで設定した`postgresSuperUserPsw`を入力します。
   レプリケーション状態を確認します。

    ```
    postgres=# SELECT pg_is_in_recovery();
     pg_is_in_recovery 
    -------------------
     f
    (1 row)
    ```
    `f`が返っているのはPRIMARYとして動作しているということです。
    PRIMARY上でテーブルを作成してみます。
    
    ```
    postgres=# CREATE TABLE tbl(a integer, b text);
    postgres=# INSERT INTO tbl VALUES(1, 'AAA');
    ```
* 次にREPLICAに接続してみます。
    やり方はPRIMARYのときと同様です。REPLICAには`mypostgres-replica`というホスト名でアクセスできます。REPLICAは複数台ありますが、Kubegresがそのうちのひとつを自動で選択してくれます。

    ```shell
    $ kubectl exec -it psql-pod -- psql -h mypostgres-replica -U postgres
    ```
    レプリケーション状態を確認すると、`t`が返りREPLICAとして動作していることが確認できます。

    ```
    postgres=# SELECT pg_is_in_recovery();
     pg_is_in_recovery 
    -------------------
     t
    (1 row)
    ```
    PRIMARY上で作成したテーブルをSEELCTしてみると、REPLICA上でも見ることができます。

    ```
    postgres=# SELECT * FROM tbl;
     a |  b  
    ---+-----
     1 | AAA
    (1 row)
    ```

# 自動フェイルオーバーの確認
ふたつの方法でPRIMARYに異常を発生させてみます。

1. PRIMARYのPodを落とす方法
1. PRIMARYのPostgreSQLのプロセスを終了させる方法

## PRIMARYのPodを落としてみる
1. まずPRIMARYのPodを特定します。
    以下のコマンドを実行すると、`Endpoints`が`172.17.0.6`になっています。

    ```shell
    $ kubectl describe services mypostgres
    Name:              mypostgres
    Namespace:         default
    Labels:            app=mypostgres
                       replicationRole=primary
    Annotations:       <none>
    Selector:          app=mypostgres,replicationRole=primary
    Type:              ClusterIP
    IP Families:       <none>
    IP:                None
    IPs:               None
    Port:              <unset>  5432/TCP
    TargetPort:        5432/TCP
    Endpoints:         172.17.0.6:5432
    Session Affinity:  None
    Events:            <none>
    ```
    「[PostgreSQLへのアクセス](#PostgreSQLへのアクセス)」の冒頭で確認したPod状態と照らし合わせると、PRIMARYのPodはmypostgres-1-0であることがわかります。

1. 次のコマンドでPodを落とします。

    ```shell
    $ kubectl delete pod mypostgres-1-0
    pod "mypostgres-1-0" deleted
    ```
1. Podの状態を確認します。
mypostgres-1-0を落とした直後は、mypostgres-2-0とmypostgres-3-0のみしかありませんが、しばらくするとmypostgres-4-0が自動で立ち上がります。

    ```shell
    $ kubectl get pods -o wide
    NAME             READY   STATUS    RESTARTS   AGE   IP           NODE       NOMINATED NODE   READINESS GATES
    mypostgres-2-0   1/1     Running   0          16s   172.17.0.6   hostname   <none>           <none>
    mypostgres-3-0   1/1     Running   0          69m   172.17.0.8   hostname   <none>           <none>
    mypostgres-4-0   1/1     Running   0          16s   172.17.0.7   hostname   <none>           <none>
    ```
1. PRIMARYを確認します。
これまでと同様に、ホスト名に`mypostgres`を指定してPRIMARYに接続してみます。

    ```shell
    $ kubectl exec -it psql-pod -- psql -h mypostgres -U postgres
    postgres=# SELECT pg_is_in_recovery();
     pg_is_in_recovery 
    -------------------
     f
    (1 row)
    ```
    このようにPodが終了する前と同じ手段でアクセスできることが確認できました。
    以下のように、mypostgres-2-0が新しくPRIMARYになっていました。

    ```shell
    $ kubectl describe services mypostgres | grep Endpoints
    Endpoints:         172.17.0.6:5432
    ```

このように、自動フェイルオーバー・自動フェイルバックが行われることが確認できました。
[PostgreSQLへのアクセス](#PostgreSQLへのアクセス)のときの状態と比べてると分かるように、PRIMARYのPod名は変わるものの、以前と変わらず同じホスト名`mypostgres`でアクセスできます。

## PRIMARYのPostgreSQLのプロセスを終了させてみる
1. 次のコマンドでPod内のプロセスを終了させます。

    ```shell
    $ kubectl exec -it mypostgres-2-0 -- /bin/bash
    root@mypostgres-2-0:/# kill 1
    ```
1. Podの状態を確認します。
プロセスをキルすると、以下のように自動的にPodが終了します。

    ```shell
    $ kubectl get pods -o wide
    NAME             READY   STATUS        RESTARTS   AGE     IP           NODE       NOMINATED NODE   READINESS GATES
    mypostgres-2-0   0/1     Terminating   1          5m14s   172.17.0.6   hostname   <none>           <none>
    mypostgres-3-0   1/1     Running       0          74m     172.17.0.8   hostname   <none>           <none>
    mypostgres-4-0   1/1     Running       0          5m1s    172.17.0.7   hostname   <none>           <none>
    ```
    しばらくすると、mypostgres-5-0が自動で立ち上がります。

    ```shell
    $ kubectl get pods -o wide
    NAME             READY   STATUS    RESTARTS   AGE     IP           NODE       NOMINATED NODE   READINESS GATES
    mypostgres-3-0   1/1     Running   0          31s     172.17.0.6   hostname   <none>           <none>
    mypostgres-4-0   1/1     Running   0          5m48s   172.17.0.7   hostname   <none>           <none>
    mypostgres-5-0   1/1     Running   0          19s     172.17.0.8   hostname   <none>           <none>
    ```
    この方法でも自動フェイルオーバーが行われることが確認できました。

# REPLICAの追加
「[PostgreSQLのサーバ構成の設定と適用](#PostgreSQLのサーバ構成の設定と適用)」で使用した、`my-postgres.yaml`ファイルを編集します。  
このファイルで`replicas: 4`としてPostgreSQLサーバ数を4に変更します。

```shell
$ kubectl apply -f my-postgres.yaml
$ kubectl get pods -o wide
NAME             READY   STATUS    RESTARTS   AGE   IP           NODE       NOMINATED NODE   READINESS GATES
mypostgres-3-0   1/1     Running   0          12m   172.17.0.6   hostname   <none>           <none>
mypostgres-4-0   1/1     Running   0          17m   172.17.0.7   hostname   <none>           <none>
mypostgres-5-0   1/1     Running   0          11m   172.17.0.8   hostname   <none>           <none>
mypostgres-6-0   1/1     Running   0          14s   172.17.0.9   hostname   <none>           <none>
```
mypostgres-6-0というPodが立ち上がり、4台構成とすることができました。

# Dockerイメージのアップデート
稼働中のPostgreSQLのバージョン（PodのDockerイメージ）を更新してみます。

## 準備
まず、現在使用しているDockerイメージを確認します。postgres:13.2であることがわかりました。

```shell
$ kubectl get pods -o=jsonpath='{range .items[*]}{"\n"}{.metadata.name}{":\t"}{range .spec.containers[*]}{.image}{", "}{end}{end}' |sort

mypostgres-3-0:	postgres:13.2, 
mypostgres-4-0:	postgres:13.2, 
mypostgres-5-0:	postgres:13.2, 
mypostgres-6-0:	postgres:13.2, 
```

今回は新しいイメージとして`postgres:myversion`を作成して、Podに反映させてみます。  

DockerイメージのIDを確認し、

```shell
$ docker images | grep postgres
postgres                                  13.2       26c8bcd8b719   2 weeks ago     314MB
postgres                                  latest     26c8bcd8b719   2 weeks ago     314MB
```
新しくタグを切ります。

```shell
$ docker tag 26c8bcd8b719 postgres:myversion
```

## アップデートの実施
1. `my-postgres.yaml`で、`image: postgres:myversion`に変更し、適用します。

    ```shell
    $ kubectl apply -f my-postgres.yaml
    ```
1. Podの状態を確認すると、Podがひとつずつ終了して再起動されます。

    ```shell
    $ kubectl get pods -o wide
    NAME             READY   STATUS        RESTARTS   AGE   IP           NODE       NOMINATED NODE   READINESS GATES
    mypostgres-3-0   1/1     Running       0          27m   172.17.0.6   hostname   <none>           <none>
    mypostgres-4-0   1/1     Running       0          32m   172.17.0.7   hostname   <none>           <none>
    mypostgres-5-0   1/1     Running       0          26m   172.17.0.8   hostname   <none>           <none>
    mypostgres-6-0   0/1     Terminating   0          15m   172.17.0.9   hostname   <none>           <none>
    ```
1. すべてのPodが再起動されたあと、Dockerイメージを確認します。

    ```shell
    $ kubectl get pods -o=jsonpath='{range .items[*]}{"\n"}{.metadata.name}{":\t"}{range .spec.containers[*]}{.image}{", "}{end}{end}' |sort

    mypostgres-3-0:	postgres:myversion, 
    mypostgres-4-0:	postgres:myversion, 
    mypostgres-5-0:	postgres:myversion, 
    mypostgres-6-0:	postgres:myversion, 
    ```
すべてのPodのDockerイメージが`postgres:myversion`となり、ローリングアップデートが行われることが確認できました。

# おわりに
Kubegresを使ってPostgreSQL4台でHAクラスタを簡単に構成することができました。
また、Kubegresで自動フェイルオーバー・自動フェイルバック、REPLICA追加およびローリングアップデートを行えることを確認しました。
今回のデフォルトの設定では、PostgreSQL上にはpostgresユーザ、postgresデータベースが作成されます。
任意のユーザあるいはデータベースを作成して起動させる方法は、別途紹介したいと思います。

# 参考文献
[https://www.kubegres.io/](https://www.kubegres.io/)
[https://kubernetes.io/](https://kubernetes.io/)
[https://minikube.sigs.k8s.io/](https://minikube.sigs.k8s.io/)
[https://www.sraoss.co.jp/tech-blog/pgsql/kubernetes-postgres-operator/](https://www.sraoss.co.jp/tech-blog/pgsql/kubernetes-postgres-operator/)
[https://qiita.com/Esfahan/items/f5c846088281c39f73a4](https://qiita.com/Esfahan/items/f5c846088281c39f73a4)