From 98229ca44a449962d926bcd667c3c0d7971fd0d7 Mon Sep 17 00:00:00 2001 From: Christopher Tineo Date: Thu, 13 Aug 2026 00:26:47 -0400 Subject: [PATCH 1/2] feat(infra): install CloudNativePG operator and shared-cluster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First half of moving Balancer off AWS RDS (CodeForPhilly/balancer-main#526). Production still reads from RDS; this only stands the cluster up beside it. Mirrors cfp-sandbox-cluster's layout: chart v0.28.0 as a holosource, projected to _infra/cloudnative-pg/operator and rendered by a helm3 lens with CRDs included; the Cluster CR and namespace sit outside the lens root and pass through raw. Two deliberate omissions, both documented inline: - no managed.roles — the balancer role's passwordSecret must be sealed first, or cnpg reports a reconcile error until it exists - no backup stanza — needs an object store and credentials that do not exist yet. Nothing may depend on this cluster as a sole copy until that lands. Storage class is pinned to linode-block-storage-retain rather than taking the cluster default, which sandbox does. Refs CodeForPhilly/balancer-main#526 --- .claude/CLAUDE.md | 2 +- .../_infra/cloudnative-pg/operator.toml | 4 ++++ .holo/lenses/cloudnative-pg.toml | 14 +++++++++++ .holo/sources/cloudnative-pg-chart.toml | 3 +++ _infra/cloudnative-pg/namespaces.yaml | 4 ++++ _infra/cloudnative-pg/shared-cluster.yaml | 24 +++++++++++++++++++ 6 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 .holo/branches/k8s-manifests/_infra/cloudnative-pg/operator.toml create mode 100644 .holo/lenses/cloudnative-pg.toml create mode 100644 .holo/sources/cloudnative-pg-chart.toml create mode 100644 _infra/cloudnative-pg/namespaces.yaml create mode 100644 _infra/cloudnative-pg/shared-cluster.yaml diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index de96526..b3da150 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -140,7 +140,7 @@ Things not in any single grep-able file: - **Wildcard DNS**: `*.live.k8s.phl.io` → the Envoy LB `45.79.246.168`. DNS is managed in OpenTofu at [CodeForPhilly/ops](https://github.com/CodeForPhilly/ops) → `tofu/dns`; a host with no specific record simply follows the wildcard, so new apps need no DNS change at all. - **Apex domains in tree**: `balancerproject.org`, `choosenativeplants.com` (+ `www.`), `codeforphilly.org` (+ `www.`), `penn-chime.phl.io`, `vaultwarden.phl.io`, `bitwarden.phl.io`. Apex ACME challenges only work once DNS points at Envoy — plan cutover and cert issuance together for these. `choosenativeplants.com` is at **Namecheap**, not Cloud DNS, so it can't be moved from the ops repo. - **A new hostname is briefly down between DNS and cert.** The cert can't issue until the hostname resolves to Envoy (Let's Encrypt has to reach the solver), and Envoy's HTTPS listener doesn't program until the cert Secret exists — meanwhile HTTP 301s into a listener that isn't there. Roughly 60–90s. Keep TTLs at 60s. -- **No cnpg / shared-cluster** on this cluster yet. If a database is needed, it ships per-app (e.g. vaultwarden runs its own PostgreSQL StatefulSet via the gissilabs chart; chime + third-places similar). +- **cnpg is landing.** `_infra/cloudnative-pg/` installs the operator (chart v0.28.0) and a `shared-cluster` Cluster, mirroring cfp-sandbox-cluster. It has **no backup configuration** — do not let it hold the only copy of anything until an object store lands (CodeForPhilly/balancer-main#526). Existing apps still ship their own database per-app (vaultwarden runs a PostgreSQL StatefulSet via the gissilabs chart; chime + third-places similar); nothing has been migrated onto the shared cluster yet. ## Guardrails diff --git a/.holo/branches/k8s-manifests/_infra/cloudnative-pg/operator.toml b/.holo/branches/k8s-manifests/_infra/cloudnative-pg/operator.toml new file mode 100644 index 0000000..71776e8 --- /dev/null +++ b/.holo/branches/k8s-manifests/_infra/cloudnative-pg/operator.toml @@ -0,0 +1,4 @@ +[holomapping] +holosource = "cloudnative-pg-chart" +root = "charts/cloudnative-pg" +files = "**" diff --git a/.holo/lenses/cloudnative-pg.toml b/.holo/lenses/cloudnative-pg.toml new file mode 100644 index 0000000..2227b87 --- /dev/null +++ b/.holo/lenses/cloudnative-pg.toml @@ -0,0 +1,14 @@ +[hololens] +container = "ghcr.io/hologit/lenses/helm3:latest" + +[hololens.input] +root = "_infra/cloudnative-pg/operator" +files = "**" + +[hololens.output] +merge = "replace" + +[hololens.helm] +namespace = "cloudnative-pg" +release_name = "cloudnative-pg" +include_crds = true diff --git a/.holo/sources/cloudnative-pg-chart.toml b/.holo/sources/cloudnative-pg-chart.toml new file mode 100644 index 0000000..1ac9f69 --- /dev/null +++ b/.holo/sources/cloudnative-pg-chart.toml @@ -0,0 +1,3 @@ +[holosource] +url = "https://github.com/cloudnative-pg/charts.git" +ref = "refs/tags/cloudnative-pg-v0.28.0" diff --git a/_infra/cloudnative-pg/namespaces.yaml b/_infra/cloudnative-pg/namespaces.yaml new file mode 100644 index 0000000..e689a91 --- /dev/null +++ b/_infra/cloudnative-pg/namespaces.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: cloudnative-pg diff --git a/_infra/cloudnative-pg/shared-cluster.yaml b/_infra/cloudnative-pg/shared-cluster.yaml new file mode 100644 index 0000000..7540c9a --- /dev/null +++ b/_infra/cloudnative-pg/shared-cluster.yaml @@ -0,0 +1,24 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: shared-cluster + namespace: cloudnative-pg +spec: + instances: 2 + imageName: ghcr.io/cloudnative-pg/postgis:18-3-system-trixie + + storage: + # Explicit, unlike sandbox, which takes the cluster default. `-retain` keeps + # the Linode volume if the PVC is ever deleted — on this cluster that is the + # difference between an incident and an outage. + storageClass: linode-block-storage-retain + size: 20Gi + + # No `managed.roles` yet: the balancer role's passwordSecret has to be sealed + # into cloudnative-pg.secrets/ first, and cnpg reports a role reconcile error + # for as long as the Secret it names is absent. The role and the balancer + # Database CR land together in the follow-up PR. + # + # No `backup:` stanza yet either — it needs an object store bucket plus + # credentials that do not exist. This cluster must not hold the only copy of + # production data until that lands. See CodeForPhilly/balancer-main#526. From 9904c729f6179ea4f5aa268e3677f0fec51c8197 Mon Sep 17 00:00:00 2001 From: Chris Alfano Date: Mon, 28 Sep 2026 10:42:31 -0400 Subject: [PATCH 2/2] feat(cnpg): upgrade operator to 1.30.1 and install Barman Cloud plugin Mirrors CodeForPhilly/cfp-sandbox-cluster#214. Chart cloudnative-pg-v0.29.1 ships operator 1.30.1; in-tree barmanObjectStore is removed in 1.31, so backups go through the Barman Cloud plugin (chart plugin-barman-cloud-v0.8.0). Both helm lenses render against kube 1.34, since the charts declare kubeVersion >=1.29. Co-Authored-By: Claude Fable 5.1 --- .claude/CLAUDE.md | 2 +- .../_infra/cloudnative-pg/barman-cloud.toml | 4 ++++ .holo/lenses/barman-cloud.toml | 20 +++++++++++++++++++ .holo/lenses/cloudnative-pg.toml | 3 +++ .holo/sources/barman-cloud-chart.toml | 3 +++ .holo/sources/cloudnative-pg-chart.toml | 2 +- 6 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 .holo/branches/k8s-manifests/_infra/cloudnative-pg/barman-cloud.toml create mode 100644 .holo/lenses/barman-cloud.toml create mode 100644 .holo/sources/barman-cloud-chart.toml diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index b3da150..550626f 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -140,7 +140,7 @@ Things not in any single grep-able file: - **Wildcard DNS**: `*.live.k8s.phl.io` → the Envoy LB `45.79.246.168`. DNS is managed in OpenTofu at [CodeForPhilly/ops](https://github.com/CodeForPhilly/ops) → `tofu/dns`; a host with no specific record simply follows the wildcard, so new apps need no DNS change at all. - **Apex domains in tree**: `balancerproject.org`, `choosenativeplants.com` (+ `www.`), `codeforphilly.org` (+ `www.`), `penn-chime.phl.io`, `vaultwarden.phl.io`, `bitwarden.phl.io`. Apex ACME challenges only work once DNS points at Envoy — plan cutover and cert issuance together for these. `choosenativeplants.com` is at **Namecheap**, not Cloud DNS, so it can't be moved from the ops repo. - **A new hostname is briefly down between DNS and cert.** The cert can't issue until the hostname resolves to Envoy (Let's Encrypt has to reach the solver), and Envoy's HTTPS listener doesn't program until the cert Secret exists — meanwhile HTTP 301s into a listener that isn't there. Roughly 60–90s. Keep TTLs at 60s. -- **cnpg is landing.** `_infra/cloudnative-pg/` installs the operator (chart v0.28.0) and a `shared-cluster` Cluster, mirroring cfp-sandbox-cluster. It has **no backup configuration** — do not let it hold the only copy of anything until an object store lands (CodeForPhilly/balancer-main#526). Existing apps still ship their own database per-app (vaultwarden runs a PostgreSQL StatefulSet via the gissilabs chart; chime + third-places similar); nothing has been migrated onto the shared cluster yet. +- **cnpg is landing.** `_infra/cloudnative-pg/` installs the operator (chart v0.29.1, operator 1.30.1), the Barman Cloud plugin (chart plugin-barman-cloud-v0.8.0), and a `shared-cluster` Cluster, mirroring cfp-sandbox-cluster. Backups go through the plugin, never the in-tree `barmanObjectStore` (removed in cnpg 1.31). Until an `ObjectStore` + `ScheduledBackup` land it has **no backup configuration** — do not let it hold the only copy of anything until an object store lands (CodeForPhilly/balancer-main#526). Existing apps still ship their own database per-app (vaultwarden runs a PostgreSQL StatefulSet via the gissilabs chart; chime + third-places similar); nothing has been migrated onto the shared cluster yet. ## Guardrails diff --git a/.holo/branches/k8s-manifests/_infra/cloudnative-pg/barman-cloud.toml b/.holo/branches/k8s-manifests/_infra/cloudnative-pg/barman-cloud.toml new file mode 100644 index 0000000..7e4c317 --- /dev/null +++ b/.holo/branches/k8s-manifests/_infra/cloudnative-pg/barman-cloud.toml @@ -0,0 +1,4 @@ +[holomapping] +holosource = "barman-cloud-chart" +root = "charts/plugin-barman-cloud" +files = "**" diff --git a/.holo/lenses/barman-cloud.toml b/.holo/lenses/barman-cloud.toml new file mode 100644 index 0000000..8d3f3dd --- /dev/null +++ b/.holo/lenses/barman-cloud.toml @@ -0,0 +1,20 @@ +[hololens] +container = "ghcr.io/hologit/lenses/helm3:latest" + +[hololens.input] +root = "_infra/cloudnative-pg/barman-cloud" +files = "**" + +[hololens.output] +merge = "replace" + +# The plugin must run in the operator's namespace. Its CRDs (ObjectStore) ship +# as templates gated on crds.create, and its gRPC mTLS certs come from a +# self-signed cert-manager Issuer the chart creates. +[hololens.helm] +namespace = "cloudnative-pg" +release_name = "barman-cloud" +include_crds = true +# The helm3 lens renders against 1.22 by default; this chart declares +# kubeVersion >=1.29. Match the cluster (LKE 1.34). +kube_version = "1.34" diff --git a/.holo/lenses/cloudnative-pg.toml b/.holo/lenses/cloudnative-pg.toml index 2227b87..d4332c8 100644 --- a/.holo/lenses/cloudnative-pg.toml +++ b/.holo/lenses/cloudnative-pg.toml @@ -12,3 +12,6 @@ merge = "replace" namespace = "cloudnative-pg" release_name = "cloudnative-pg" include_crds = true +# The helm3 lens renders against 1.22 by default; chart >=0.29 declares +# kubeVersion >=1.29. Match the cluster (LKE 1.34). +kube_version = "1.34" diff --git a/.holo/sources/barman-cloud-chart.toml b/.holo/sources/barman-cloud-chart.toml new file mode 100644 index 0000000..84ec111 --- /dev/null +++ b/.holo/sources/barman-cloud-chart.toml @@ -0,0 +1,3 @@ +[holosource] +url = "https://github.com/cloudnative-pg/charts.git" +ref = "refs/tags/plugin-barman-cloud-v0.8.0" diff --git a/.holo/sources/cloudnative-pg-chart.toml b/.holo/sources/cloudnative-pg-chart.toml index 1ac9f69..64c167a 100644 --- a/.holo/sources/cloudnative-pg-chart.toml +++ b/.holo/sources/cloudnative-pg-chart.toml @@ -1,3 +1,3 @@ [holosource] url = "https://github.com/cloudnative-pg/charts.git" -ref = "refs/tags/cloudnative-pg-v0.28.0" +ref = "refs/tags/cloudnative-pg-v0.29.1"