Skip to content

Commit 875a9dd

Browse files
committed
feat: single db as primary and crunchy as secondary
1 parent 0c5dcee commit 875a9dd

File tree

21 files changed

+552
-47
lines changed

21 files changed

+552
-47
lines changed

.github/workflows/.deployer.yml

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,18 @@ jobs:
8888
name: Stack
8989
environment: ${{ inputs.environment }}
9090
runs-on: ubuntu-24.04
91+
env:
92+
# Repo-level toggle: Settings -> Variables -> Actions
93+
# Expected values: 'single' or 'crunchy'
94+
DB_PROVIDER: ${{ vars.DB_PROVIDER || 'single' }}
9195
outputs:
9296
tag: ${{ inputs.tag || steps.pr.outputs.pr }}
9397
triggered: ${{ steps.deploy.outputs.triggered }}
9498
steps:
9599
- uses: bcgov/action-crunchy@a109166ff26880dc75676492a33b816ac0ce392f # v1.2.6
96100
name: Deploy Crunchy
97101
id: deploy_crunchy
102+
if: env.DB_PROVIDER == 'crunchy'
98103
with:
99104
oc_namespace: ${{ secrets.OC_NAMESPACE }}
100105
oc_token: ${{ secrets.OC_TOKEN }}
@@ -120,15 +125,44 @@ jobs:
120125
# version, to support helm packaging for non-pr based releases (workflow_dispatch). default to 1.0.0+github run number
121126
version=1.0.0+${{ github.run_number }}
122127
128+
# Database provider toggle
129+
db_provider="${DB_PROVIDER}"
130+
if [ "${db_provider}" != "single" ] && [ "${db_provider}" != "crunchy" ]; then
131+
echo "Unsupported DB_PROVIDER='${db_provider}'. Expected 'single' or 'crunchy'." >&2
132+
exit 1
133+
fi
134+
135+
# Database alias used by the Helm chart:
136+
# - crunchy: points at operator-created Secrets/Services
137+
# - single: points at in-release Service named '${release}-db'
138+
crunchy_release="${{ steps.deploy_crunchy.outputs.release }}"
139+
if [ "${db_provider}" = "crunchy" ]; then
140+
databaseAlias="${crunchy_release}-crunchy"
141+
db_password=""
142+
db_name=""
143+
else
144+
databaseAlias="db"
145+
db_name="app"
146+
# Keep it URL-safe and helm-flag-safe (no quotes/spaces)
147+
db_password=$(openssl rand -hex 24)
148+
fi
149+
123150
# Summary
124151
echo "tag=${tag}"
125152
echo "release=${release}"
126153
echo "version=${version}"
154+
echo "db_provider=${db_provider}"
155+
echo "databaseAlias=${databaseAlias}"
156+
[ -z "${db_name}" ] || echo "db_name=${db_name}"
127157
128158
# Output
129159
echo "tag=${tag}" >> $GITHUB_OUTPUT
130160
echo "release=${release}" >> $GITHUB_OUTPUT
131161
echo "version=${version}" >> $GITHUB_OUTPUT
162+
echo "db_provider=${db_provider}" >> $GITHUB_OUTPUT
163+
echo "databaseAlias=${databaseAlias}" >> $GITHUB_OUTPUT
164+
echo "db_name=${db_name}" >> $GITHUB_OUTPUT
165+
echo "db_password=${db_password}" >> $GITHUB_OUTPUT
132166
133167
- name: Stop pre-existing deployments on PRs (status = pending-upgrade)
134168
if: github.event_name == 'pull_request'
@@ -172,11 +206,17 @@ jobs:
172206
helm package -u . --app-version="tag-${{ steps.vars.outputs.tag }}_run-${{ github.run_number }}" --version=${{ steps.pr.outputs.pr || steps.vars.outputs.version }}
173207
# print the values.yaml file to see the values being used
174208
# Helm upgrade/rollout
209+
DB_PARAMS=""
210+
if [ "${{ steps.vars.outputs.db_provider }}" = "single" ]; then
211+
DB_PARAMS="--set-string global.secrets.databaseName=${{ steps.vars.outputs.db_name }} --set-string global.secrets.databasePassword=${{ steps.vars.outputs.db_password }}"
212+
fi
175213
helm upgrade \
176214
--set-string global.repository=${{ github.repository }} \
177215
--set-string global.tag="${{ steps.vars.outputs.tag }}" \
178216
--set-string global.config.databaseUser="${{ inputs.db_user }}" \
179-
--set-string global.databaseAlias="${{ steps.deploy_crunchy.outputs.release }}-crunchy" \
217+
--set-string global.database.provider="${{ steps.vars.outputs.db_provider }}" \
218+
--set-string global.databaseAlias="${{ steps.vars.outputs.databaseAlias }}" \
219+
${DB_PARAMS} \
180220
${{ inputs.params }} \
181221
--install --wait ${{ inputs.atomic && '--atomic' || '' }} ${{ steps.vars.outputs.release }} \
182222
--timeout ${{ inputs.timeout-minutes }}m \

.github/workflows/demo.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
oc_namespace: ${{ secrets.oc_namespace }}
3333
oc_token: ${{ secrets.oc_token }}
3434
oc_server: ${{ vars.oc_server }}
35-
command: |
35+
commands: |
3636
oc delete route/${{ env.REPO }}-${{ env.DEST }} --ignore-not-found=true
3737
oc create route edge ${{ env.REPO }}-${{ env.DEST }} \
3838
--hostname=${{ env.REPO }}-${{ env.DEST }}.${{ env.DOMAIN }} \

.github/workflows/merge.yml

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ on:
44
push:
55
branches: [main]
66
paths-ignore:
7-
- '*.md'
8-
- '.github/**'
9-
- '.github/graphics/**'
10-
- '!.github/workflows/**'
7+
- "*.md"
8+
- ".github/**"
9+
- ".github/graphics/**"
10+
- "!.github/workflows/**"
1111
workflow_dispatch:
1212
inputs:
1313
tag:
1414
description: "Image tag set to deploy; e.g. PR number or prod"
1515
type: string
16-
default: 'prod'
16+
default: "prod"
1717

1818
concurrency:
1919
# Do not interrupt previous workflows
@@ -48,8 +48,7 @@ jobs:
4848
with:
4949
environment: prod
5050
db_user: appproxy # appproxy is the user which works with pgbouncer.
51-
params:
52-
--set backend.deploymentStrategy=RollingUpdate
51+
params: --set backend.deploymentStrategy=RollingUpdate
5352
--set frontend.deploymentStrategy=RollingUpdate
5453
--set global.autoscaling=true
5554
--set frontend.pdb.enabled=true
@@ -64,7 +63,7 @@ jobs:
6463
packages: write
6564
strategy:
6665
matrix:
67-
package: [migrations, backend, frontend]
66+
package: [migrations, backend, frontend, database, pgbouncer]
6867
timeout-minutes: 1
6968
steps:
7069
- uses: shrink/actions-docker-registry-tag@f04afd0559f66b288586792eb150f45136a927fa # v4

.github/workflows/pr-open.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
runs-on: ubuntu-24.04
2020
strategy:
2121
matrix:
22-
package: [backend, frontend, migrations]
22+
package: [backend, frontend, migrations, database, pgbouncer]
2323
timeout-minutes: 10
2424
steps:
2525
- uses: bcgov/action-builder-ghcr@2b24ac7f95e6a019064151498660437cca3202c5 # v4.2.1
@@ -40,14 +40,14 @@ jobs:
4040
with:
4141
db_user: app-${{ github.event.number }}
4242
params: --set global.secrets.persist=false
43-
triggers: ('backend/' 'frontend/' 'migrations/' 'charts/' '.github/workflows/.deployer.yml')
43+
triggers: ('backend/' 'frontend/' 'migrations/' 'database/' 'pgbouncer/' 'charts/' '.github/workflows/.deployer.yml')
4444
db_triggers: ('charts/crunchy/')
4545

4646
tests:
47-
name: Tests
48-
if: needs.deploys.outputs.triggered == 'true'
49-
needs: [deploys]
50-
uses: ./.github/workflows/.tests.yml
47+
name: Tests
48+
if: needs.deploys.outputs.triggered == 'true'
49+
needs: [deploys]
50+
uses: ./.github/workflows/.tests.yml
5151

5252
results:
5353
name: PR Results

README.md

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ This repository provides a template to rapidly deploy a modern web application s
2828
* 🔍 Self-healing through probes/checks (startup, readiness, liveness)
2929
* 🎯 Point the long-lived DEMO route to PRs by using the `demo` label
3030
* **Sample application stack:**
31-
* 🗄️ Database: Crunchy (Postgres, PostGIS), backups, Flyway
31+
* 🗄️ Database: Single Postgres/PostGIS (default) or Crunchy (optional), backups, Flyway
3232
* 🎨 Frontend: TypeScript, Caddy Server with Coraza WAF
3333
* ⚙️ Backend: TypeScript, Nest.js
3434
* 🔄 Alternative backend examples - see [Alternative Backends](#alternative-backends)
@@ -152,6 +152,16 @@ OpenShift server address (API endpoint for your OpenShift cluster).
152152
* Example values (BCGov): `https://api.gold.devops.gov.bc.ca:6443` or `https://api.silver.devops.gov.bc.ca:6443`
153153
* For other OpenShift clusters: Use your cluster's API server address (typically `https://api.<cluster-domain>:6443`)
154154

155+
**`DB_PROVIDER`** 🗄️
156+
157+
Database provider toggle for deployments.
158+
159+
* Where: `Settings > Secrets and Variables > Actions > Variables`
160+
* Values: `single` (default) or `crunchy`
161+
* Behavior:
162+
* `single`: Deploys an in-release Postgres/PostGIS StatefulSet with PgBouncer sidecar (no Crunchy operator resources).
163+
* `crunchy`: Deploys Crunchy via `bcgov/action-crunchy` and wires the app to the Crunchy-generated service/secrets.
164+
155165
## 🔄 Updating Dependencies
156166

157167
Dependabot and Mend Renovate can both provide dependency updates using pull requests. Dependabot is simpler to configure, while Renovate is much more configurable and lighter on resources.
@@ -454,10 +464,14 @@ The starter stack includes a frontend (React, Bootstrap, Vite, Caddy), backend (
454464
* 💪 [TypeScript](https://www.typescriptlang.org/) strong-typing for JavaScript
455465
* 🏗️ [NestJS](https://docs.nestjs.com) Nest/Node backend and frontend
456466
* 🔄 [Flyway](https://flywaydb.org/) database migrations
457-
* 🐘 [Crunchy](https://www.crunchydata.com/products/crunchy-postgresql-for-kubernetes) Postgres/PostGIS Database
467+
* 🗄️ Database provider toggle (`single` or `crunchy`)
468+
* 🐘 [Crunchy](https://www.crunchydata.com/products/crunchy-postgresql-for-kubernetes) Postgres/PostGIS Database (optional)
469+
* 🐘 Single Postgres/PostGIS (default) with PgBouncer sidecar
458470
* 🛡️ [OWASP Coraza WAF](https://github.com/corazawaf/coraza-caddy) Web Application Firewall integrated with Caddy
459471

460-
PostGIS is enabled by default for geospatial data support when postGISVersion value is provided. To switch to standard PostgreSQL, update the `postGISVersion` field in the [Crunchy Helm chart values](./charts/crunchy/values.yml) to `~`. This disables PostGIS extensions, making it a plain PostgreSQL setup.
472+
PostGIS notes:
473+
* **Single mode** uses the repository-owned database image (built from `./database/Dockerfile`), currently targeting Postgres 18 + PostGIS.
474+
* **Crunchy mode** uses `charts/crunchy/values.yml`. PostGIS is enabled when `crunchy.postGISVersion` is set; to switch to standard PostgreSQL, set it to `~`.
461475

462476
### 🛡️ OWASP Coraza WAF: Application Security
463477

@@ -504,9 +518,34 @@ The WAF is integrated directly into the Caddy web server, providing real-time pr
504518

505519
For more details, see the [Coraza documentation](https://coraza.io/docs/).
506520

507-
## 🗄️ Crunchy Database
521+
## 🗄️ Database
522+
523+
This template supports two database providers. The provider is controlled via the repository variable `DB_PROVIDER` and is also passed into Helm as `global.database.provider`.
524+
525+
### 🐘 Single Postgres/PostGIS (default)
526+
527+
This mode deploys a NON-HA database **inside the application Helm release**:
528+
529+
* A Postgres/PostGIS **StatefulSet** and **Service** named `<release>-db`
530+
* A **PgBouncer sidecar** (service port 5432 targets PgBouncer)
531+
* Optional backups via a **CronJob** using the BCGov backup container (`ghcr.io/bcgov/backup-container`)
532+
533+
Images:
534+
* Database image is built from `./database/Dockerfile` and published to GHCR (expected default: `ghcr.io/<org>/<repo>/database:<tag>`)
535+
* PgBouncer image is built from `./pgbouncer/Dockerfile` and published to GHCR (expected default: `ghcr.io/<org>/<repo>/pgbouncer:<tag>`)
536+
537+
Backups (single mode):
538+
* Enable with Helm values: `global.database.backup.enabled=true`
539+
* Configure schedule/retention/PVC under `global.database.backup.*` in `charts/app/values.yaml`
540+
* Optional object storage settings live under `global.database.backup.s3.*`
541+
Pros:
542+
* Simple to maintain, low resource consumption
543+
Cons:
544+
* Non HA: https://developer.gov.bc.ca/docs/default/component/platform-developer-docs/docs/automation-and-resiliency/app-resiliency-guidelines/#a-highly-available-application
545+
546+
### 🐘 Crunchy Database (optional)
508547

509-
Crunchy is the default choice for high availability (HA) Postgres/PostGIS databases in BC Government.
548+
Crunchy is still the recommended choice for high availability (HA) Postgres/PostGIS databases in BC Government with its known pitfalls and drawbacks around operational overhead.
510549

511550
### 🌟 Key Features
512551
- ⚡ Automatic failover with Patroni
@@ -521,11 +560,7 @@ Crunchy is the default choice for high availability (HA) Postgres/PostGIS databa
521560
3. **🚨 DR Testing**: Disaster Recovery Testing is **`MANDATORY`** before go live.
522561

523562
### 💾 Enabling S3 Backups
524-
To enable S3 backups/recovery, provide these parameters to the GitHub Action:
525-
- `s3_access_key`
526-
- `s3_secret_key`
527-
- `s3_bucket`
528-
- `s3_endpoint`
563+
To enable S3 backups/recovery in Crunchy mode, configure `crunchy.pgBackRest.s3.*` in `charts/crunchy/values.yml` (or an environment-specific override values file).
529564

530565
> **Important**: Never reuse the same s3/object store, bucket path across different Crunchy deployments or instances (dev, test, prod)
531566

charts/app/templates/_helpers.tpl

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,45 @@ Selector labels
4444
app.kubernetes.io/name: {{ include "fullname" . }}
4545
app.kubernetes.io/instance: {{ .Release.Name }}
4646
{{- end }}
47+
48+
{{/*
49+
Database component helpers (single-db mode)
50+
*/}}
51+
{{- define "db.name" -}}
52+
{{- printf "db" -}}
53+
{{- end }}
54+
55+
{{- define "db.selectorLabels" -}}
56+
app.kubernetes.io/name: {{ include "db.name" . }}
57+
app.kubernetes.io/instance: {{ .Release.Name }}
58+
{{- end }}
59+
60+
{{- define "db.labels" -}}
61+
{{ include "db.selectorLabels" . }}
62+
{{- if .Chart.AppVersion }}
63+
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
64+
{{- end }}
65+
app.kubernetes.io/managed-by: {{ .Release.Service }}
66+
app.kubernetes.io/component: db
67+
{{- end }}
68+
69+
{{/*
70+
Database backup component helpers
71+
*/}}
72+
{{- define "dbbackup.name" -}}
73+
{{- printf "db-backup" -}}
74+
{{- end }}
75+
76+
{{- define "dbbackup.selectorLabels" -}}
77+
app.kubernetes.io/name: {{ include "dbbackup.name" . }}
78+
app.kubernetes.io/instance: {{ .Release.Name }}
79+
{{- end }}
80+
81+
{{- define "dbbackup.labels" -}}
82+
{{ include "dbbackup.selectorLabels" . }}
83+
{{- if .Chart.AppVersion }}
84+
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
85+
{{- end }}
86+
app.kubernetes.io/managed-by: {{ .Release.Service }}
87+
app.kubernetes.io/component: db-backup
88+
{{- end }}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{{- if and (eq (default "single" .Values.global.database.provider) "single") (.Values.global.database.backup.enabled | default false) }}
2+
---
3+
apiVersion: batch/v1
4+
kind: CronJob
5+
metadata:
6+
name: {{ .Release.Name }}-db-backup
7+
labels: {{- include "dbbackup.labels" . | nindent 4 }}
8+
spec:
9+
schedule: {{ default "0 8 * * *" .Values.global.database.backup.schedule | quote }}
10+
concurrencyPolicy: Forbid
11+
successfulJobsHistoryLimit: 1
12+
failedJobsHistoryLimit: 3
13+
jobTemplate:
14+
spec:
15+
template:
16+
metadata:
17+
labels: {{- include "dbbackup.labels" . | nindent 12 }}
18+
spec:
19+
restartPolicy: OnFailure
20+
containers:
21+
- name: backup
22+
image: {{ default "ghcr.io/bcgov/backup-container:2.11.0" .Values.global.database.backup.image }}
23+
imagePullPolicy: {{ default "IfNotPresent" .Values.global.database.backup.imagePullPolicy }}
24+
env:
25+
- name: BACKUP_STRATEGY
26+
value: {{ default "rolling" .Values.global.database.backup.strategy | quote }}
27+
- name: DAILY_BACKUPS
28+
value: {{ default "6" .Values.global.database.backup.retention.daily | quote }}
29+
- name: WEEKLY_BACKUPS
30+
value: {{ default "4" .Values.global.database.backup.retention.weekly | quote }}
31+
- name: MONTHLY_BACKUPS
32+
value: {{ default "1" .Values.global.database.backup.retention.monthly | quote }}
33+
- name: BACKUP_DIR
34+
value: "/backups"
35+
- name: DATABASE_SERVICE_NAME
36+
value: {{ printf "%s-db" .Release.Name | quote }}
37+
envFrom:
38+
- secretRef:
39+
name: {{ .Release.Name }}-db-backup
40+
volumeMounts:
41+
- name: backups
42+
mountPath: /backups
43+
resources: {{- toYaml (.Values.global.database.backup.resources | default dict) | nindent 16 }}
44+
volumes:
45+
- name: backups
46+
persistentVolumeClaim:
47+
claimName: {{ .Release.Name }}-db-backups
48+
{{- end }}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{{- if and (eq (default "single" .Values.global.database.provider) "single") (.Values.global.database.backup.enabled | default false) }}
2+
---
3+
apiVersion: v1
4+
kind: PersistentVolumeClaim
5+
metadata:
6+
name: {{ .Release.Name }}-db-backups
7+
labels: {{- include "labels" . | nindent 4 }}
8+
spec:
9+
accessModes:
10+
- ReadWriteOnce
11+
resources:
12+
requests:
13+
storage: {{ default "25Gi" .Values.global.database.backup.persistence.size }}
14+
{{- if .Values.global.database.backup.persistence.storageClassName }}
15+
storageClassName: {{ .Values.global.database.backup.persistence.storageClassName }}
16+
{{- end }}
17+
{{- end }}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{{- if and (eq (default "single" .Values.global.database.provider) "single") (.Values.global.database.backup.enabled | default false) }}
2+
{{- $databaseUser := required "global.config.databaseUser is required" .Values.global.config.databaseUser }}
3+
{{- $databasePassword := required "global.secrets.databasePassword is required for single-db" .Values.global.secrets.databasePassword }}
4+
{{- $databaseName := default "app" .Values.global.secrets.databaseName }}
5+
---
6+
apiVersion: v1
7+
kind: Secret
8+
metadata:
9+
name: {{ .Release.Name }}-db-backup
10+
labels: {{- include "labels" . | nindent 4 }}
11+
type: Opaque
12+
data:
13+
POSTGRESQL_USER: {{ $databaseUser | b64enc | quote }}
14+
POSTGRESQL_PASSWORD: {{ $databasePassword | b64enc | quote }}
15+
POSTGRESQL_DATABASE: {{ $databaseName | b64enc | quote }}
16+
{{- if .Values.global.database.backup.s3.enabled }}
17+
S3_USER: {{ required "global.database.backup.s3.accessKey is required when s3.enabled=true" .Values.global.database.backup.s3.accessKey | b64enc | quote }}
18+
S3_PASSWORD: {{ required "global.database.backup.s3.secretKey is required when s3.enabled=true" .Values.global.database.backup.s3.secretKey | b64enc | quote }}
19+
S3_ENDPOINT: {{ required "global.database.backup.s3.endpoint is required when s3.enabled=true" .Values.global.database.backup.s3.endpoint | b64enc | quote }}
20+
S3_BUCKET: {{ required "global.database.backup.s3.bucket is required when s3.enabled=true" .Values.global.database.backup.s3.bucket | b64enc | quote }}
21+
{{- end }}
22+
{{- end }}

0 commit comments

Comments
 (0)