Zalando Postgres Operatorの紹介

本記事では、コンテナ管理ツールのKubernetes上でPostgreSQLのHAクラスタを構築できるZalando Postgres Operatorについて解説します。

Zalando Postgres Operatorとは

Zalando Postgres Operator (以下 Postgres Operator)は、コンテナの管理ツールであるKubernetes上でHA構成のPostgreSQLクラスタを容易に構築できるようにするoperatorです(operatorはKubernetesの機能を拡張するプラグインのような機構です)。本ソフトウェアはドイツのファッション系eコマース企業であるZalando SEによって開発され、自社のサービスで運用されています。本ソフトウェアはMITライセンスの元公開されています。

Operatorについて

OperatorはKubernetes上で動作するアプリケーションの各種管理を自動化するための機能です。従来システム管理者が手動で行っていた、監視、障害時の対応、バックアップ、スケーリングといった様々な操作をプログラムとして記述し、Kubernetesに組み込むことでKubernetesの動作を拡張できます。

また、PostgreSQLに新たなoperatorが必要となるもう一つの要因として、PostgreSQLクラスタはプライマリとレプリカ(スタンバイ)の役割を考慮する必要があり、ステートレスなアプリケーション(例:Webサーバ)のように簡単にレプリカ(複製)を複数台用意することができないことが挙げられます。

Postgres Operatorの機能

Postgres Operatorによりストリーミングレプリケーションを用いたプライマリ1台、レプリカ(スタンバイ)複数台のPostgreSQLクラスタを簡単に構築することができます。

Zalando Postgres Operatorの構成

Postgres Operatorの主な機能は以下になります。

  • PostgreSQL HAクラスタの管理
  • ストリーミングレプリケーション (同期・非同期)
  • コネクションプール (PgBouncer使用)
  • フェイルオーバー (pg_rewindにより高速な復旧が可能)
  • ローリングアップデート
  • 論理バックアップ (S3使用)
  • WALアーカイブ・PITR (WAL-E、S3使用)

Zalando社による講演

PGCon 2020カンファレンスイベントにてZalando社により行われた講演の資料が公開されています。こちらもあわせて参照ください。
PostgreSQL on K8s at Zalando: Two years in production

検証環境

本記事ではKubernetesのマネージドサービスであるAmazon EKS上でZalando Postgres Operatorを動作させます。
本記事では以下の構成で解説を行います。

  • Kubernetes: Amazon EKS (Kubernetesのマネージドサービス)
  • ワーカーノード: t3.medium 2ノード
  • アーカイブ用ストレージ: Amazon S3
  • 作業環境: CentOS 8.1 (VirtualBox上のVM)

なお、EKSクラスタの作成は既に完了しているものとします。EKSの使用方法についてはこちらを参照ください。

検証内容

今回はPostgres Operatorの以下の項目について確認しました。

  • Postgres Operator のデプロイ
  • Web UI
  • PostgreSQL クラスタの作成
  • PostgreSQLの動作確認
  • フェイルオーバー
  • スケーリング
  • ローリングアップグレード
  • 論理バックアップ
  • WALアーカイブ・PITR
  • ログ

Postgres Operatorのデプロイ

Postgres OperatorはKubernetesとしては基本的な構成のため、導入は比較的簡単です。

Githubのリポジトリを作業環境にcloneします(もしくはtarballを展開します)。

$ git clone https://github.com/zalando/postgres-operator.git
$ cd postgres-operator/

グローバルな設定はconfigmap.yamlファイルに記述されているので、必要に応じて修正します。
ここではバックアップ先のS3の設定などのグローバルな設定を記述します。

kubectl create -fで各YAMLファイルを適用していきます。

$ kubectl create -f configmap.yaml
$ kubectl create -f operator-service-account-rbac.yaml
$ kubectl create -f postgres-operator.yaml
$ kubectl create -f api-service.yaml

オブジェクトが作成され、正常に動作していることを確認します。

$ kubectl get all
NAME                                     READY   STATUS    RESTARTS   AGE
pod/postgres-operator-85978df8cf-zqwjd   1/1     Running   0          48s

NAME                        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/kubernetes          ClusterIP   10.100.0.1       <none>        443/TCP    168m
service/postgres-operator   ClusterIP   10.100.197.174   <none>        8080/TCP   22s

NAME                                READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/postgres-operator   1/1     1            1           48s

NAME                                           DESIRED   CURRENT   READY   AGE
replicaset.apps/postgres-operator-85978df8cf   1         1         1       49s

Web UI

Postgres Operatorには Web UIが同梱されており、ブラウザから以下の操作が可能です。

  • クラスタの作成・削除
  • 複製
  • 設定編集
  • 状態確認

Postgres Operator UI

Web UIのデプロイは同様にkubectlを使用して行います。

$ kubectl create -f ui/manifests/

Postgresクラスタの作成

PostgresクラスタのデプロイはYAMLファイルを作成して適用するか、前述のWeb UIで作成操作を行います。

サンプルのYAMLファイルが同梱されていますので、それを元に設定ファイルを作成します。

  • minimal-postgres-manifest.yaml: 最小限の設定
  • complete-postgres-manifest.yaml: 各種設定を網羅
  • standby-manifest.yaml: スタンバイ専用クラスタ設定

基本的な設定を含む設定ファイルは以下になります。

apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
  name: sraoss-minimal-cluster           # クラスタ名の指定
spec:
  teamId: "sraoss"
  volume:
    size: 1Gi                            # ボリュームサイズの指定
  numberOfInstances: 2                   # インスタンス数(マスター+レプリカ)の指定
  users:                                 # DBユーザーの作成
    zalando:  # database owner
    - superuser
    - createdb
    foo_user: []  # role for application foo
  databases:                             # データベースの作成
    foo: zalando  # dbname: owner
  preparedDatabases:
    bar: {}
  postgresql:
    version: "12"                        # PostgreSQLバージョンの指定(9.6~12)
  enableLogicalBackup: true              # 論理バックアップの有効化
  logicalBackupSchedule: "30 00 * * * *" # cron形式のバックアップスケジュール
  enableConnectionPooler: true           # コネクションプールの有効化
  connectionPooler:
    numberOfInstances: 2

これに加え、PostgreSQLのpostgresql.conf、pg_hba.confに相当する追加の設定を行うこともできます。

...
  postgresql:
    version: "12“
    parameters:                          # postgresql.conf内のパラメータの指定
      shared_buffers: "64MB"
      max_connections: "50"
      log_statement: "all“
  patroni:
    initdb:                              # initdb時のパラメータの指定
      encoding: "UTF8"
      locale: "ja_JP.UTF-8"
      data-checksums: "true"
    pg_hba:                              # pg_hba.conf内のパラメータの指定
    - hostssl all all 0.0.0.0/0 md5
    - host    all all 0.0.0.0/0 md5

同様にkubectlで適用します。

$ kubectl create -f test-postgres-manifest.yaml

クラスタの状態を確認します。numberOfInstances: 2の場合は2つのPodが作成されます。各Podがmasterかreplicaかはspilo-roleラベルで確認できます。

$ kubectl get postgresql
NAME                     TEAM     VERSION   PODS   VOLUME   CPU-REQUEST   MEMORY-REQUEST   AGE    STATUS
sraoss-minimal-cluster   sraoss   12        2      1Gi                                     113s   Running

$ kubectl get pods -l application=spilo -L spilo-role
NAME                       READY   STATUS    RESTARTS   AGE     SPILO-ROLE
sraoss-minimal-cluster-0   1/1     Running   0          2m19s   master
sraoss-minimal-cluster-1   1/1     Running   0          95s     replica

PostgreSQLの動作確認

Kubernetesクラスタ内の他のPodからは上記のmasterおよびreplicaにアクセスすることができます。

作業環境からpsqlでアクセスしたい場合はkubectlのport-forward機能を利用します。
※ PGMASTERにはmasterのPod名が入ります。

$ export PGMASTER=$(kubectl get pods -o jsonpath={.items..metadata.name} -l application=spilo,cluster-name=sraoss-minimal-cluster,spilo-role=master)
$ kubectl port-forward $PGMASTER 6432:5432

別端末からlocalhost:6432に接続します。なお、接続時にはSSLが必須となります。
※ パスワードはPostgresクラスタ作成時に自動生成されます。

$ kubectl get secret postgres.sraoss-minimal-cluster.credentials -o 'jsonpath={.data.password}' | base64 -d
(パスワード文字列が表示される)
$ export PGSSLMODE=require
$ psql -U postgres -h localhost -p 6432
ユーザ postgres のパスワード:(上記パスワード文字列)
postgres=# create table t (t text); postgres=# insert into t values ('hoge'); postgres=# select * from t; t ------ hoge (1 行)

フェイルオーバー

masterのPodに故障が発生した場合は、自動的にフェイルオーバーしてreplicaがmasterに昇格します。これはPostgres Operatorが使用している、SpiloというPostgreSQLおよびHAを実現するためのテンプレート(Patroni)を含むDockerイメージにより実現しています。

master Podを削除した場合は元replicaがmasterに昇格し、削除したPodが再作成されてreplicaとして復帰します。データサイズが少ない状態ではあるものの、フェイルオーバーは数10秒程度ですばやく完了します。

$ kubectl delete pod sraoss-minimal-cluster-0
pod "sraoss-minimal-cluster-0" deleted

$ kubectl get pods -l application=spilo -L spilo-role
NAME                       READY   STATUS              RESTARTS   AGE   SPILO-ROLE
sraoss-minimal-cluster-0   0/1     ContainerCreating   0          6s
sraoss-minimal-cluster-1   1/1     Running             0          12m   master

$ kubectl get pods -l application=spilo -L spilo-role
NAME                       READY   STATUS    RESTARTS   AGE   SPILO-ROLE
sraoss-minimal-cluster-0   1/1     Running   0          23s   replica
sraoss-minimal-cluster-1   1/1     Running   0          12m   master

masterのプロセス故障が発生した場合は元replicaがmasterに昇格し、元masterはPodを維持したままプロセスを再起動しreplicaに降格します。以下の例ではmaster Pod内に入り、initプロセスを停止させて疑似的にプロセス故障を起こしています。

$ kubectl exec sraoss-minimal-cluster-0 -it /bin/sh
# kill 1

スケーリング

デプロイ済みのPostgresクラスタの設定を変更すると、変更内容に応じてクラスタの状態を自動的に追従させます。
クラスタのPodの数を指定するnumberOfInstancesを2から3に変更すると、Statefulsetsのレプリカ数も2から3に変更され、新しいreplica Podが起動します。

$ kubectl edit postgresql
spec:
  numberOfInstances: 2 -> 3 に修正

$ kubectl get postgresql
NAME                     TEAM     VERSION   PODS   VOLUME   CPU-REQUEST   MEMORY-REQUEST   AGE   STATUS
sraoss-minimal-cluster   sraoss   12        3      1Gi                                     53m   Running
$ kubectl get statefulsets
NAME                     READY   AGE
sraoss-minimal-cluster   3/3     14m
$ kubectl get po -l application=spilo -L spilo-role
NAME                       READY   STATUS    RESTARTS   AGE     SPILO-ROLE
sraoss-minimal-cluster-0   1/1     Running   0          50m     master
sraoss-minimal-cluster-1   1/1     Running   0          50m     replica
sraoss-minimal-cluster-2   1/1     Running   0          7m21s   replica

ローリングアップデート

PostgreSQLのマイナーバージョンアップ(Dockerイメージの更新)、およびPostgreSQLの設定変更はローリングアップデートによりサービスの停止なしに行うことができます。なお、メジャーバージョンアップはローリングアップデートでは行えないため、ベースバックアップからクラスタの複製により行う必要があります。

ローリングアップデート

以下、Dockerイメージを更新した場合にローリングアップデートが行われる様子を実際に確認してみます。

Dockerイメージの更新

$ kubectl edit postgresql sraoss-test-cluster
  dockerImage: (旧Dockerイメージ) -> (新Dockerイメージ)
  postgresql:
    version: "12"

ローリングアップデート開始

$ kubectl get postgresql
NAME                  TEAM     VERSION   PODS   VOLUME   CPU-REQUEST   MEMORY-REQUEST   AGE     STATUS
sraoss-test-cluster   sraoss   12        2      1Gi                                     9m19s   Updating

$ kubectl get pods -l application=spilo -L spilo-role
NAME                    READY   STATUS        RESTARTS   AGE     SPILO-ROLE
sraoss-test-cluster-0   1/1     Running       0          9m24s   master
sraoss-test-cluster-1   1/1     Terminating   0          8m42s

$ kubectl get pods -l application=spilo -L spilo-role
NAME                    READY   STATUS              RESTARTS   AGE     SPILO-ROLE
sraoss-test-cluster-0   1/1     Running             0          9m34s   master
sraoss-test-cluster-1   0/1     ContainerCreating   0          8s

元replica Podが再作成され、masterに昇格

$ kubectl get pods -l application=spilo -L spilo-role
NAME                    READY   STATUS    RESTARTS   AGE     SPILO-ROLE
sraoss-test-cluster-0   1/1     Running   0          9m39s
sraoss-test-cluster-1   1/1     Running   0          13s     promoted

元master Podの再作成

$ kubectl get pods -l application=spilo -L spilo-role
NAME                    READY   STATUS        RESTARTS   AGE     SPILO-ROLE
sraoss-test-cluster-0   1/1     Terminating   0          9m49s
sraoss-test-cluster-1   1/1     Running       0          23s     master

$ kubectl get pods -l application=spilo -L spilo-role
NAME                    READY   STATUS              RESTARTS   AGE   SPILO-ROLE
sraoss-test-cluster-0   0/1     ContainerCreating   0          4s
sraoss-test-cluster-1   1/1     Running             0          30s   master

元master Podがreplicaとして復帰

$ kubectl get pods -l application=spilo -L spilo-role
NAME                    READY   STATUS    RESTARTS   AGE   SPILO-ROLE
sraoss-test-cluster-0   1/1     Running   0          14s   replica
sraoss-test-cluster-1   1/1     Running   0          40s   master

論理バックアップ

Postgres Operatorでは定期的にAmazon S3に論理バックアップ(pg_dumpall)を取ることができます。なお、論理バックアップからの自動リストア機能はないので、リストアはクラスタ作成後手動で行う必要があります。

前述のconfigmap.yamlでバックアップ先のS3の設定を行います。

logical_backup_s3_bucket: "zalando-backup"
logical_backup_s3_access_key_id: "xxxxxxxxxxxxxxxx"
logical_backup_s3_secret_access_key: "xxxxxxxxxxxxxxxx

Postgresクラスタの設定で論理バックアップを有効にします。スケジュールはcron表記で指定します(以下は毎日0:30に実行)。

  enableLogicalBackup: true
  logicalBackupSchedule: “30 00 * * *"

指定時刻にKubernetesのCronJobとしてバックアップが実行されます。

$ kubectl get cronjob
NAME                                    SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
logical-backup-sraoss-minimal-cluster   30 00 * * *   False     0        6m48s           27m

$ kubectl get job
NAME                                               COMPLETIONS   DURATION   AGE
logical-backup-sraoss-minimal-cluster-1593418800   1/1           3s         6m46s

$ kubectl describe job logical-backup-sraoss-minimal-cluster-1593418800
...
Pods Statuses:  0 Running / 1 Succeeded / 0 Failed

S3の以下の場所にバックアップファイルがアップロードされます。

s3://<bucket-name>/spilo/<cluster-name>/<UUID>/logical_backups/<job ID>.sql.gz

WALアーカイブ・PITR

前述のconfigmap.yamlでバックアップ先のS3の設定を行います。なお、EKSクラスタからS3 bucketへのアクセスが許可されている必要があります。

wal_s3_bucket: "zalando-wal-backup"

S3の以下の場所にWALがアーカイブされます。

s3://<bucket-name>/spilo/<cluster-name>/<UUID>/wal/basebackups_00x/
s3://<bucket-name>/spilo/<cluster-name>/<UUID>/wal/wal_00x/

S3のバックアップからクラスタを複製(clone)することでPITRが可能です。PITRの復元位置を指定することもできます。

spec:
  clone:
    uid: "efd12e58-5786-11e8-b5a7-06148230260c"
    cluster: “sraoss-test-cluster"
    timestamp: "2017-12-19T12:40:33+01:00"

WALアーカイブからのclone

ログ

Postges Operator自体はログ管理に関する機能を持ちません。Podのsidecarコンテナをユーザが追加することは可能なので、ログ収集のための補助コンテナを導入し、fluentdと連携するなどの作り込みは必要となります。また、Kubernetesクラスタのログを集約するKubernetes Log WatcherというツールがZalando社によって公開されています。

クラスタの各Podのログはkubectl logsコマンドにより確認できます。

$ kubectl logs sraoss-minimal-cluster-0

また、Pod内のPostgreSQLログは以下のように格納されています。

/home/postgres/pgdata/pgroot/pg_log/
-rw-r--r-- 1 postgres postgres      0 Jun 29 06:12 postgresql-0.csv
-rw-r--r-- 1 postgres postgres 277783 Jun 29 06:58 postgresql-1.csv
-rw-r--r-- 1 postgres postgres    205 Jun 29 06:12 postgresql-1.log
-rw-r--r-- 1 postgres postgres      0 Jun 29 06:12 postgresql-2.csv
-rw-r--r-- 1 postgres postgres      0 Jun 29 06:12 postgresql-3.csv
-rw-r--r-- 1 postgres postgres      0 Jun 29 06:12 postgresql-4.csv
-rw-r--r-- 1 postgres postgres      0 Jun 29 06:12 postgresql-5.csv
-rw-r--r-- 1 postgres postgres      0 Jun 29 06:12 postgresql-6.csv
-rw-r--r-- 1 postgres postgres      0 Jun 29 06:12 postgresql-7.csv

おわりに

手動でPostgreSQL HAクラスタを構築するとなるとかなりの手間がかかります。また、Kubernetes上にそれを構築しようとするのはかなり困難です。Zalando Postgres Operatorを用いることで、Kubernetes上のPostgreSQL HAクラスタの構築が非常に容易になります。実際に開発元のサービスで運用されている実績があり、機能的にまだ不十分なところもあるものの、フェイルオーバーなどの重要な機能に関してはしっかりと作り込まれていると感じました。今後の発展が楽しみなソフトウェアといえます。