📝

PostgreSQL on Kubernetes を 試してみた

2022/12/02に公開

この記事は MICIN Advent Calendar 2022 の2日目の記事です。

前回は、あの日見たerbを僕達はまだ知らない。でした。

そして PostgreSQLアドベントカレンダー 2022 の2日目とのマルチポストでもあります。

PostgreSQLアドベントカレンダーの前回の記事は、noborusさんのPostgreSQL 15 日本語マニュアルあり〼でした。

はじめに

先日、PostgreSQL Conference Japan 2022 に一般参加をしてきました。PostgreSQL Conferenceに参加し始めて5年ほどたちますが、普段業務ではキャッチアップしきれないような最新情報のキャッチアップをはじめ、勉強になることが多く、非常に刺激的な1日を過ごすことができました。

その中で、【B2】PGOを用いたPostgreSQL on Kubernetes入門 というセッションを聴講し、「自分もKubernetesでPostgreSQLを動かしてみたい!」と思いました。

MICINでは通常RDS for PostgreSQLを利用していますし、なかなか自前のPostgreSQLを、しかもKubernetes上で運用するということもなさそうではあるのですが、自分の中にいろいろな選択肢を持っておきたいというのが主なモチベーションです。

せっかくなので、セッションで学んだPGOそのままではなく、その次くらいにStarがついているZalandoのpostgres_operator を使ってやってみようかな。

そんな感じで、PostgreSQLアドベントカレンダーの記事としてはあまり目新しさがないかもしれませんが、やってみようと思います。

この記事で扱うこと

  • kind + postgres_operator で PostgreSQL on Kubernetes を試してみる。

  • (+アルファでなにか)

この記事で扱わないこと

  • Kubernetesとはなにか

  • Kubernetes の 基本操作

  • PostgreSQL on Kubernetes のメリット・デメリット(の詳細)

1.kindのinstall

https://kind.sigs.k8s.io/docs/user/quick-start/

まずはローカルでkubernetesを動かす環境を用意しなければなりません。

kindというDocker上でKubernetesクラスタを構築できるツールがあるようなので、これを利用します。

こちらはKubernetesの公式プロジェクトで、マルチノードクラスタを簡単に作成できる、テストやCIのために使いやすいツールです。

リンク先の指示に従ってインストールしてみます。

コマンド一発でkindのクラスタを作ったらあとはkubectlでマニュアル操作するだけなので、「Kubernetesの使い方を試してみる」には非常にお手軽でうれしいツールです。

$ kind create cluster
$ kubectl get all 
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   58s

これだけでローカルのKubernetesクラスタが動くので非常にうれしいですね。

While deploying Zalando getting issue- could not create cluster: could not create pod disruption budget #2098

2022/11/23 現在、こちらのissueが改善されていないようです。

kindでクラスタを作成する際にベースイメージを指定してKubernetes 1.23以前で動くようにしました。

kind create cluster --image kindest/node:v1.23.13  

2. postgres_operator by zalando を使ってみる

今回は、https://github.com/zalando/postgres-operator で公開されている postgres_operator を使ってみようと思います。

ざっくりと調べてみたところ、UIがあるのがPGOとの大きな違いのようです。

また、PostgreSQLの 自動フェイルオーバークラスタを構築するのに有用な Patroni もZalandoが開発しているので、そのあたりの連携なんかも期待できるかもしれません。

readme に書かれている機能から抜粋すると、

  • ローリングアップデートでマイナーバージョンアップを迅速にできます

  • メジャーバージョンアップの場合にはインプレースアップグレードをサポートしています

  • podの再起動を伴わずにボリュームのリサイズができます(AWS EBS, PVC)

  • PGBouncerを利用したコネクションプーリングを利用できます

  • 論理バックアップをS3やGCSに保存できます。

  • S3やGCSにあるWALアーカイブからstandby クラスタを構築できます

  • Patroniを利用したストリーミングレプリケーション構成を構築できます。

  • Spiloとpg_basebackup / WAL-E を利用してPoint-In-Time-Recoveryができます。

といったところで、非常に魅力的に思えます。

postgres-operatorを構築する

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

まずはclone してきて、quickstart に沿ってコマンドを打ってみましょう。

$ kubectl create -f manifests/configmap.yaml
configmap/postgres-operator created

$ kubectl create -f manifests/operator-service-account-rbac.yaml 
serviceaccount/postgres-operator created
clusterrole.rbac.authorization.k8s.io/postgres-operator created
clusterrolebinding.rbac.authorization.k8s.io/postgres-operator created
clusterrole.rbac.authorization.k8s.io/postgres-pod created

$ kubectl create -f manifests/postgres-operator.yaml 
deployment.apps/postgres-operator created

$ kubectl create -f manifests/api-service.yaml
service/postgres-operator created

ここまで適用したら、状況を確認してみましょう。

create した オブジェクトが稼働している様子がうかがえます。

$ kubectl get all 
NAME                                     READY   STATUS    RESTARTS      AGE
pod/postgres-operator-5c7d5d756c-bqqnf   1/1     Running   1 (10s ago)   22s

NAME                        TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
service/kubernetes          ClusterIP   10.96.0.1      <none>        443/TCP    2m35s
service/postgres-operator   ClusterIP   10.96.160.71   <none>        8080/TCP   8s

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

NAME                                           DESIRED   CURRENT   READY   AGE
replicaset.apps/postgres-operator-5c7d5d756c   1         1         1       22s


operator UI を デプロイする

zalandoのpostgres_operatorには管理UIがついているらしいので使ってみましょう。

以下のコマンドでデプロイできます。

$ kubectl apply -k ui/manifests 
serviceaccount/postgres-operator-ui created
clusterrole.rbac.authorization.k8s.io/postgres-operator-ui created
clusterrolebinding.rbac.authorization.k8s.io/postgres-operator-ui created
service/postgres-operator-ui created
deployment.apps/postgres-operator-ui created
ingress.networking.k8s.io/postgres-operator-ui created
$ kubectl get pod -l name=postgres-operator-ui
NAME                                    READY   STATUS    RESTARTS   AGE
postgres-operator-ui-64898f986c-qx54m   1/1     Running   0          98s

uiのpodが立ち上がりました。local にポートフォワーディングして、localからアクセスできるようにしてみましょう。

kubectl port-forward は リターンせずにそのままターミナルを占有するので、必ず別のタブなりウィンドウなりを開いて実行しましょう。 & もききませんでした……

https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/

Note: kubectl port-forward does not return. To continue with the exercises, you will need to open another terminal.
$ kubectl port-forward svc/postgres-operator-ui 8081:80
Forwarding from 127.0.0.1:8081 -> 8081
Forwarding from [::1]:8081 -> 8081

アクセスするとこんな感じの画面が出てきます。

フォームに入力すると、右側のyamlが動的に更新されていきます。

IaCを推進するにあたってこういうのがあるととっつきやすくて非常によいですね。

PostgreSQL Cluster を作ってみる。

quick start の続きを見ると、以下のコマンドで postgres cluster を作ってみようとあります。

$ kubectl create -f manifests/minimal-postgres-manifest.yaml

多分これを実行すると PostgreSQLのクラスタが立ち上がるのだとは思いますが、まずはこのyamlに何がかいてあるのか見てみましょう。

postgres_operatorの細かい設定は必要になってから調べてもいいですが、PostgreSQLの設定はある程度認識しておきたいですからね。

apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
  name: acid-minimal-cluster
  namespace: default
spec:
  teamId: "acid"
  volume:
    size: 1Gi
  numberOfInstances: 2
  users:
    zalando:  # database owner
    - superuser
    - createdb
    foo_user: []  # role for application foo
  databases:
    foo: zalando  # dbname: owner
  preparedDatabases:
    bar: {}
  postgresql:
    version: "14"

というわけでこんな感じです。

リファレンスはこちら

PostgreSQL14 のインスタンスが2台、zalandoというユーザーにfooというデータベースができそうですね。ボリュームも1Giということでカジュアルに実行してもローカルのリソースを圧迫したりしなそうです。

ちょっと実行してみましょう。

$ kubectl create -f manifests/minimal-postgres-manifest.yaml 
postgresql.acid.zalan.do/acid-minimal-cluster created
$ kubectl get postgresql 
NAME                   TEAM   VERSION   PODS   VOLUME   CPU-REQUEST   MEMORY-REQUEST   AGE     STATUS
acid-minimal-cluster   acid   14        2      1Gi                                     2m16s   Creating

version 14, pods が2つ、Volumeは1Gi、たしかに書かれている通りですね。StatusがCreatingなのでしばらく待ってみましょう。

$ kubectl get postgresql 
NAME                   TEAM   VERSION   PODS   VOLUME   CPU-REQUEST   MEMORY-REQUEST   AGE    STATUS
acid-minimal-cluster   acid   14        2      1Gi                                     4m5s   Running

数分待っていたら、無事Runningになりました。

こちらの記述を元に接続をしてみましょう。

 $ export PGMASTER=$(kubectl get pods -o jsonpath={.items..metadata.name} -l application=spilo,cluster-name=acid-minimal-cluster,spilo-role=master -n default)
 $ kubectl port-forward $PGMASTER 6432:5432 -n default 
$ export PGPASSWORD=$(kubectl get secret postgres.acid-minimal-cluster.credentials.postgresql.acid.zalan.do -o 'jsonpath={.data.password}' | base64 -d)
$ psql -U postgres -h localhost -p 6432              

psql (14.5)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=# 

無事につながりました。

schemaやテーブルをみると、ログやcron、メトリックや管理情報などがテーブルになっているようで、非常にうれしいです。

postgres=# \dn
      List of schemas
      Name       |  Owner   
-----------------+----------
 cron               | postgres
 metric_helpers  | postgres
 public            | postgres
 user_management | postgres
 zmon_utils      | postgres
(5 rows)

postgres=# \dt
            List of relations
 Schema |     Name     | Type  |  Owner   
--------+--------------+-------+----------
 public | postgres_log | table | postgres
(1 row)

設定を管理する

PostgreSQLの設定と言えば、 postgresql.conf や、 pg_hba.confですよね。

manifestでそのあたりも指定できるみたいです。

こんな感じで my-postgres-manifest.yaml を作って適用してみましょう。

ついでに、いかにも(公式)サンプルっぽい名前のところを「俺のマニフェストだ!」って感じに変更してみます。

気をつけたいところとしては、クラスタ名は teamId- で始まる必要があるという規約でしょうか。

それから、pg_hba.confの設定を追加するときには、replicationの行を入れておかないとstandbyのPodからプライマリのPodに接続できなくてクラスタの構築に失敗してしまいます。

自前でレプリケーションを構築する手順を思い出せば当たり前なのですが、意識せずにレプリケーションが設定されていることもあってうっかりしてしまいました。

apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
  name: sugai-postgresql-cluster
  namespace: default
spec:
  teamId: "sugai"
  volume:
    size: 1Gi
  numberOfInstances: 2
  users:
    sugai:  # database owner
    - superuser
    - createdb
    reader: []
  databases:
    my_database: sugai  # dbname: owner
  preparedDatabases:
    prepared: {}
  postgresql:
    version: "14"
    parameters: 
      shared_buffers: "64MB"
      max_connections: "20"
      log_statement: "all"
  patroni:
    initdb:
      encoding: "UTF8"
      locale: "C"
      data-checksums: "true"
    pg_hba:
    - local   all all trust
    - hostssl all all 0.0.0.0/0 md5
    - host    all all 0.0.0.0/0 md5
    - host    all all ::1/128   md5
    - host    replication standby 0.0.0.0/0 md5
$ kubectl create -f manifests/my-postgres-manifest.yaml  

このyamlを適用するとこんな感じになりました。

$ kubectl get all
NAME                                       READY   STATUS    RESTARTS     AGE
pod/postgres-operator-7c854475f8-cqfth     1/1     Running   1 (3h ago)   3h
pod/postgres-operator-ui-b9dd88cf4-b6272   1/1     Running   0            3h
pod/sugai-postgresql-cluster-0             1/1     Running   0            15m
pod/sugai-postgresql-cluster-1             1/1     Running   0            14m

NAME                                      TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/kubernetes                        ClusterIP   10.96.0.1       <none>        443/TCP    3h1m
service/postgres-operator                 ClusterIP   10.96.129.152   <none>        8080/TCP   3h
service/postgres-operator-ui              ClusterIP   10.96.247.37    <none>        80/TCP     3h
service/sugai-postgresql-cluster          ClusterIP   10.96.123.62    <none>        5432/TCP   15m
service/sugai-postgresql-cluster-config   ClusterIP   None            <none>        <none>     14m
service/sugai-postgresql-cluster-repl     ClusterIP   10.96.134.139   <none>        5432/TCP   15m

NAME                                   READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/postgres-operator      1/1     1            1           3h
deployment.apps/postgres-operator-ui   1/1     1            1           3h

NAME                                             DESIRED   CURRENT   READY   AGE
replicaset.apps/postgres-operator-7c854475f8     1         1         1       3h
replicaset.apps/postgres-operator-ui-b9dd88cf4   1         1         1       3h

NAME                                        READY   AGE
statefulset.apps/sugai-postgresql-cluster   2/2     15m

NAME                                                TEAM    VERSION   PODS   VOLUME   CPU-REQUEST   MEMORY-REQUEST   AGE   STATUS
postgresql.acid.zalan.do/sugai-postgresql-cluster   sugai   14        2      1Gi                                     15m   Running

postgres_operator の機能をいくつか試してみる

standbyノードの追加/削除

kubectl edit コマンドを使ってもいいのですが、IaCの観点からいくとmanifestを修正してapplyする方がよいでしょう。

< numberOfInstances: 2
---
> numberOfInstances: 4

として、適用してみましょう。

$ kubectl apply -f manifests/my-postgres-manifest.yaml
postgresql.acid.zalan.do/sugai-postgresql-cluster configured
$ kubectl get postgresql
NAME                       TEAM    VERSION   PODS   VOLUME   CPU-REQUEST   MEMORY-REQUEST   AGE   STATUS
sugai-postgresql-cluster   sugai   14        4      1Gi                                     20m   Running

$ kubectl get pods 
NAME                                   READY   STATUS    RESTARTS       AGE
postgres-operator-7c854475f8-cqfth     1/1     Running   1 (3h5m ago)   3h5m
postgres-operator-ui-b9dd88cf4-b6272   1/1     Running   0              3h5m
sugai-postgresql-cluster-0             1/1     Running   0              20m
sugai-postgresql-cluster-1             1/1     Running   0              20m
sugai-postgresql-cluster-2             1/1     Running   0              29s
sugai-postgresql-cluster-3             1/1     Running   0              19s

さすがKubernetesです。細かい設定など何一つせずにシュッとPodを追加できました。

numberOfInstances を減らしてapplyするとちゃんとレプリカが減ります。

DBのレプリケーションができているかどうかも試してみましょう。

以前に試した以下のポートフォワーディング設定があります。

$ kubectl port-forward $PGMASTER 6432:5432 -n default
$ psql -U postgres -h localhost -p 6432
postgres=# create table test (id serial primary key, value text );
CREATE TABLE
postgres=# insert into test(value)values('hoge'),('fuga'),('piyo');
INSERT 0 3
postgres=# select * from test;
 id | value 
----+-------
  1 | hoge
  2 | fuga
  3 | piyo
(3 rows)

追加したレプリカにもポートフォワードしてみましょう

$ kubectl port-forward sugai-postgresql-cluster-3 7432:5432 -n default  
% psql -U postgres -h localhost -p 7432
postgres=# select * from test;
 id | value 
----+-------
  1 | hoge
  2 | fuga
  3 | piyo
(3 rows)

先ほど追加したテーブルとレコードが入っています。

postgres=# insert into test(value)values('hogehoge');
ERROR:  cannot execute INSERT in a read-only transaction

ストリーミングレプリケーションでは、もちろんレプリカにINSERTはできません。

自動フェイルオーバーのテスト

masterのpostmasterプロセスをkillしてみましょう。

$ kubectl exec -it sugai-postgresql-cluster-0 -- bash

でPodに入って

# kill 1

としてみましょう。

$ kubectl get pods -l application=spilo -L spilo-role
NAME                         READY   STATUS    RESTARTS   AGE   SPILO-ROLE
sugai-postgresql-cluster-0   1/1     Running   0          55m   
sugai-postgresql-cluster-1   1/1     Running   0          55m   replica
sugai-postgresql-cluster-2   1/1     Running   0          35m   replica
sugai-postgresql-cluster-3   1/1     Running   0          35m   master

master pod のプロセスがクラッシュしたことを受けて、即座に3番がmasterに昇格しています。

$ kubectl get pods -l application=spilo -L spilo-role
NAME                         READY   STATUS    RESTARTS       AGE   SPILO-ROLE
sugai-postgresql-cluster-0   1/1     Running   1 (107s ago)   57m   replica
sugai-postgresql-cluster-1   1/1     Running   0              57m   replica
sugai-postgresql-cluster-2   1/1     Running   0              37m   replica
sugai-postgresql-cluster-3   1/1     Running   0              37m   master

1分ほどで元masterだったpodもreplicaとしてrestartしています。

$ psql -U postgres -h localhost -p 6432               
postgres=# insert into test(value)values('hoge');
ERROR:  cannot execute INSERT in a read-only transaction

replicaになったので sugai-postgresql-cluster-0 にはInsert できません。

$ psql -U postgres -h localhost -p 7432
postgres=# insert into test(value)values('hoge');
INSERT 0 1

昇格した sugai-postgresql-cluster-3 にはInsertできるようになっています。

やり残したこと

そろそろ分量も長くなってきましたし、この記事の締め切りの時間も近づいて参りました。

本来であれば、以下のような部分もきちんと試してみたかったのですが、これは冬休みの宿題にでもしようと思います。

  • クラウドへの論理バックアップ

  • PITR

  • クラスタ間HA構成とフェイルオーバー

  • コネクションプーリング(PGBouncer)

  • オートスケーリング

  • sidecarを使ったログ収集や監視の構築

  • EKSやGKEを使って本格的に運用するときへの道しるべを作る

おわりの前に

検証用に使ったkind cluster は以下のコマンドで削除することができます。

kind delete cluster

使い始めるのも、やめるのも気軽にできてうれしいです。

おわりに

最近ではRDSやCloud SQLを業務利用することが一般的になっており、MICINでもその例に漏れず通常はAmazon RDS for PostgreSQLを利用しています。

今回はサンプル程度の検証までしかできませんでしたが、PostgreSQL on Kubernetes を構築してみることによってDBaaSの裏側を少しだけですが想像できるようになったような気がします。

また、Kubernetes や PostgreSQLのHAクラスタがこんなに手軽にローカルに作ってみることができるというのを体験できたのは非常によかったです。

MICIN Advent Calendar で明日は次田さんの【Apollo Client で複数の GraphQL API を扱う】です。

PostgreSQLアドベントカレンダーの明日は kasa_zip さんです。


MICINではメンバーを大募集しています。

「とりあえず話を聞いてみたい」でも大歓迎ですので、お気軽にご応募ください!

MICIN採用ページ:https://recruit.micin.jp/

株式会社MICIN

Discussion