CloudNative PGというPostgreSQLのoperatorを自宅で動かしているアプリ用に導入しようとして辞めた話です。
概要
最初に辞めた理由などを簡単にまとめておくと
- 自宅のk8sクラスタの可用性などを考えるとPersistentVolumeClain(PVC)を静的に作成しバックアップなどを手動で実施するほうが安心
- CloudNative PGはPVCを静的に作成する方法はあまり推奨されていない
- Clusterを削除するとPVCも一緒に削除されてしまう
上記の理由で採用を見送りました。ワークアラウンドなどもあるのですが手間が増えるし、安全・確実な方法はなさそうなので諦めました。
経緯
現在、自宅のk8sで色々アプリを動かしているのですが、基本的にStatefulsetを作って個別にPostgreSQLを立てるような運用を実施しています。大きな不満はないのですが、バックアップを作成するなどの定期作業のためにCronJobを作成したりしています。
CloudNative PGを導入することで、これらのPostgreSQLを保守する手間を削減したいと考えて導入できないかと検討を始めました。
CloudNative PGの利用方法
CloudNative PG自体はHelmを使えば簡単に導入できます。以下のようなコマンドで導入しました。
これは公式のインストール手順です。
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm upgrade --install cnpg \
--namespace cnpg-system \
--create-namespace \
cnpg/cloudnative-pg
これで、operatorの導入が出来たので、あとは Cluster という CustomResourceを作成するとPostgreSQLをCloudNative PGが作成してくれます。クラスタ構成やバックアップ設定など様々なオプションを指定できます(詳細は公式ドキュメントを参照してください)。手動で同様のものを構築するのは大変なので手軽に構築できるのは非常にありがたいです。
例えば、以下のマニフェストで3台のクラスタでそれぞれに1GBのストレージを保有しているPostgreSQLを稼働させることができます。
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-example
spec:
instances: 3
storage:
size: 1Gi
ここまでは便利だなと思っていたのですが、ストレージ回りを調べ始めるとちょっと自分の用途に合わないところが出てきました。
静的に作ったPVCで運用したい
CloudNative PGは起動すると自動的にPVCを作って動的にストレージを確保することを前提としています。ただ、静的にストレージを確保して動作することも想定されているようです。公式のドキュメントにもそのような記載があります。
自宅のk8sはdynamic provisioningのvolumeも作れるようになっているので動作は問題ないのですが、方針としてdynamic provisionされたストレージは永続のストレージにしたくないなという気持ちがあります。例えばキャッシュを保存するようなものにはdynamic provisionで構わないと考えています。
これは色々理由があるのですが、主要な理由としては故障時のサルベージが面倒くさいことです。動的に確保されたvolumeの実体にuuidなどが含まれていて、しかもどれが最新のものなのか判断するのが難しいです。CloudNative PGのように次々にPVCを作るような方針だと大量にPVCが作られてしまって故障したときなどに復旧させようとするのが難しい可能性があるので避けたいなと考えたのです。
可能であればPVCとPVを事前に固定で作成してそれをPostgreSQLに与える形式を選びたいです。こういう形であればPVが固定出来ているのでPVを個別にバックアップをする仕組みを作りこんだり、故障などの時もサルベージすべきデータの在り処がすぐに分かって便利です。
静的PVCの検証
なんと、マニュアルの通りに静的にPVCを作成してClusterを作成してみたところ起動しません。
同じ状況についてのIssueもあるようです。
https://github.com/cloudnative-pg/cloudnative-pg/issues/5235
ログなどを見ているとInit処理だけ動いて以降の処理が動いていないように見えます。
動的に確保した場合のPVCとの差分を見ていると annotation や label に差分があることが分かりました。中でも以下の cnpg.io/nodeSerial
というannotationの有無が大きいようです。
cnpg.io/nodeSerial: "1"
試しに、以下のような手順で操作してみると無事起動しました。
- PVC+PVを手動作成
- Clusterを起動
- Init処理の完了を待つ
- PVCに
cnpg.io/nodeSerial
を付与 - Clusterを再起動
また、annotationが付いていないPVCに対して、Init処理複数回実行したりすることがあり中を見るとPostgreSQLのデータ(PGDATA)が複数作られているような状態になったりもします。
さらに、Clusterを削除するとPVCが削除されてしまいます。reclaimPolicyをretainにしていなかった場合はデータがロストしてしまいます。これは避けたいところです。
全体的に何故そんな挙動になるのか理解できなかったのでCloudNativePGのソースコードを読むことにしました。
ソースコードの調査
ソースコードの詳細については省きますが、CloudNative PGは以下のような動作をしていることが分かりました。
- Clusterの状況を常に見張っているコントローラ(cluster_controller)というものがある
- コントローラのReconcile関数によってクラスタの初期化などが随時実施されるようになっている
- PVCの作成関連の処理
- Initの処理
- createPrimaryInstance関数でinitの処理が行われる
- https://github.com/cloudnative-pg/cloudnative-pg/blob/main/internal/controller/cluster_controller.go#L834-L836
- createPrimaryInstanceという名前なのにどうもJobを作成するだけのものになっている
- そのJobでPostgreSQLの初期化処理を実施している
- この中で既存のデータが存在していたらそれを別名保存し新しく作成するような処理があった
- createPrimaryInstance関数でinitの処理が行われる
- Podの起動の処理
- CloudNative PGはPodを起動する処理というのはpodのrecoveryの処理となっている
- 以下の2種類に違いは無いのでこういう処理になっていると思われる
- initが実施されたPVCを見つける
- 既に過去起動されているPVCを読み込む
- 以下の2種類に違いは無いのでこういう処理になっていると思われる
- 実際にPodを起動していると思われるのは以下
- ensureInstancesAreCreated関数
- https://github.com/cloudnative-pg/cloudnative-pg/blob/main/internal/controller/cluster_create.go#L1300
- この中でfindInstancePodToCreateが呼ばれ、その中で起動に使うPVCの選択などが実行されている
- 未所属のPVCはOrphanPVCとしてOwnerReferenceなどが設定される
- ensureInstancesAreCreated関数
- CloudNative PGはPodを起動する処理というのはpodのrecoveryの処理となっている
ここまで読んで適当に静的なPVCを与えても動かない理由が分かってきました。
ensureInstancesAreCreatedなどで色々な箇所でNodeSerialを取得している箇所があるのですが、CloudNative PGの中でこのNodeSerialを付与している箇所はCloudNative PGが作成したPVCだけです。
このNodeSerialはPostgreSQLと対応するものなので、CloudNative PG側で適当に振るわけにもいかず、データを与える側が当然知っているものということなのでしょう。
つまり、前項であった cnpg.io/nodeSerial: "1"
を付与すると起動できるというのはこのためでしょう。
次に Clusterを削除するとPVCが消えてしまうことについても、PVCを見つけるとき(OrphanedPVCの処理などで)OwnerReference付与するようです。このOwnerReferenceが付いているリソースはk8s上では依存関係があるものとして扱われ削除するときに一緒に消えてしまいます。
PVCからkubectl edit
などでOwnerReferenceを削除することで一時的に回避することはできますが、再度起動すると再度OwnerReferenceを付与してしまうため根本的には解決できません。そもそもOnwerになっていないとCloudNative PGはPVCを扱おうとしないようです。
つまり、CloudNative PGを適切に管理するには以下の事項に注意する必要があります
- Clusterを安易に削除してはならない
- 一緒にPVCが必ず削除されてしまうのでPersistentVolumeが一緒に消えないようにするか、消えても困らないような仕組みを検討する
- PVCを手動で与える場合は
cnpg.io/nodeSerial
を必ず付与する- 付与したらInitの処理が動作しないので付与する前にInitの処理を実施する
- 前項に上げた手順でClusterに実施させるのが手軽
結論
CloudNative PGのPVC周りはもう少しこなれてから導入を再検討しようかなと判断しました。
OwnerReferenceについてはもう少し良い方法はないのかなと思ったりします。ArgoCDなどを使ってClusterをデプロイしているような状況だと、色々操作した結果Replaceを選択してClusterを作り直してしまった場合などにPVCが消えてしまってトラブルになってしまいそうです。Clusterを作るHelm Chartとアプリを作る部分を別Chartにして別管理にするというのも手ですが小規模なアプリ開発ではそこまでやりたくないというのが本音です。
PVCの利用方法についてはそういうものだと諦めてInitが完了しているデータを最初に作ってしまえば以降は気にしないで運用できるということは分かったのは良かったです。reclaimPolicy を retain にすることが守れているのであれば、Clusterを消してしまってもPVは残るので、PVの claimRef を修正することで再度起動すればリカバリできるというのは今回のコードリーディングでも明確になりました。
また、今回は検討対象ではなかったので調査していないのですが、バックアップからレストアを最初にする前提の運用であればまた動作が違って運用の方法も違うかもしれません。
そもそも静的PVC辞めれば良いのではないか?
そもそも静的にPVC、PVを作成したい理由が dynamic provision している Volume のサルベージに関する不安が前提にあります。これについて何か方針が立てられるのであれば Cluster を削除しないようにする運用なら導入可能ではあるので、未来への宿題にします。