Skip to content

Add Vacation Planner: in-cluster PostgreSQL sample - #4

Merged
paolosalvatori merged 1 commit into
mainfrom
in-cluster-postgresql
Jun 4, 2026
Merged

Add Vacation Planner: in-cluster PostgreSQL sample#4
paolosalvatori merged 1 commit into
mainfrom
in-cluster-postgresql

Conversation

@paolosalvatori

Copy link
Copy Markdown
Collaborator

Motivation

The web-app-in-cluster-postgresql sample previously persisted the Vacation Planner app's data in an Azure Database for PostgreSQL flexible server, a managed Azure data service. This PR reworks the sample to instead run PostgreSQL in-cluster, as a Kubernetes StatefulSet deployed alongside the app on AKS.

This demonstrates a fully self-contained, in-cluster data tier (no managed Azure database dependency): a PostgreSQL primary with two streaming-replica standbys, backed by Azure managed-disk PersistentVolumeClaims and exposed through headless / primary (write) / read ClusterIP services. It complements the existing managed-service samples and gives a reference for teams that prefer running their own database inside the cluster.

Changes

  • Removed Azure Database for PostgreSQL flexible server from all scripts and manifests (server creation, firewall rule, managed database, FQDN lookups, admin credentials).
  • Added the in-cluster PostgreSQL StatefulSet (statefulset.yml): PostgreSQL 16 primary + 2 streaming replicas, managed-disk PVCs, and headless/primary/read services. The app connects to the primary (write) service pg-postgres-primary.
  • 00-variables.sh — dropped the flex-server variables; added in-cluster variables (StatefulSet/primary pod & service names, superuser bootstrap credentials kept in sync with the StatefulSet secret, local port-forward port).
  • 01-deploy-resources.sh — now provisions only the resource group and ACR (no managed database).
  • 05-deploy-app.sh — deploys the StatefulSet and waits for it to become ready, then (over a kubectl port-forward to the primary) creates the PlannerDB database, the dedicated testuser role and grants, the activities table, and seeds the sample data — before deploying the app. The database/role/seed provisioning runs before the app Deployment so pods can authenticate on first start.
  • 03-run-docker-container.sh — the optional local smoke test now reaches the in-cluster database through a kubectl port-forward to the primary service instead of an Azure FQDN.
  • configmap.yml / secret.yml — point at the in-cluster primary service and the testuser credential.
  • App source code — unchanged. The Flask app is fully config-driven (PG_HOST/PG_PORT/PG_USER/PG_PASSWORD/PG_DATABASE), so the existing Docker image works as-is.
  • Documentation — rewrote samples/web-app-in-cluster-postgresql/README.md, and added the sample to the backing-store list and the Samples table in the top-level README.md.

Test

The sample was successfully deployed to a real Azure Kubernetes Service (AKS) cluster on Azure and verified end-to-end:

  • The PostgreSQL StatefulSet rolled out with all 3 pods (pg-postgres-0/1/2) Running.
  • 05-deploy-app.sh created PlannerDB, the testuser role and grants, the activities table, and seeded all 9 sample rows (confirmed by the final SELECT).
  • All 3 app pods reached 1/1 Running with 0 restarts; logs show PostgreSQL schema initialized and readiness probes returning 200.
  • Browsing the port-forwarded app showed the seeded activities on first load.

Todo

Copilot AI review requested due to automatic review settings June 4, 2026 13:01
@paolosalvatori
paolosalvatori merged commit bcb82c6 into main Jun 4, 2026
1 check passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR reworks the web-app-in-cluster-postgresql sample to be self-contained by running PostgreSQL inside the AKS cluster as a 3-replica StatefulSet (1 primary + 2 streaming standbys), removing the dependency on Azure Database for PostgreSQL flexible server and updating scripts/manifests/docs accordingly.

Changes:

  • Add Kubernetes manifests for an in-cluster PostgreSQL StatefulSet with headless/primary/read Services, plus deployment-time DB/user/schema/seed provisioning via kubectl port-forward + psql.
  • Add/refresh the Vacation Planner Flask app source + HTML/CSS UI assets for this sample.
  • Update documentation and top-level README to include the new in-cluster PostgreSQL sample.

Reviewed changes

Copilot reviewed 25 out of 66 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
samples/web-app-in-cluster-postgresql/src/templates/index.html New Vacation Planner UI template (table + modals + client-side interactions).
samples/web-app-in-cluster-postgresql/src/static/style.css Styling for the new UI, including dark mode and responsive layout.
samples/web-app-in-cluster-postgresql/src/static/bootstrap/css/bootstrap-utilities.rtl.min.css Added Bootstrap static asset (utilities, RTL, minified).
samples/web-app-in-cluster-postgresql/src/static/bootstrap/css/bootstrap-utilities.min.css Added Bootstrap static asset (utilities, minified).
samples/web-app-in-cluster-postgresql/src/static/bootstrap/css/bootstrap-reboot.rtl.min.css Added Bootstrap static asset (reboot, RTL, minified).
samples/web-app-in-cluster-postgresql/src/static/bootstrap/css/bootstrap-reboot.rtl.css Added Bootstrap static asset (reboot, RTL).
samples/web-app-in-cluster-postgresql/src/static/bootstrap/css/bootstrap-reboot.min.css.map Added source map for bootstrap reboot minified CSS.
samples/web-app-in-cluster-postgresql/src/static/bootstrap/css/bootstrap-reboot.min.css Added Bootstrap static asset (reboot, minified).
samples/web-app-in-cluster-postgresql/src/static/bootstrap/css/bootstrap-reboot.css Added Bootstrap static asset (reboot).
samples/web-app-in-cluster-postgresql/src/requirements.txt Python dependencies for the Flask + psycopg2 app container.
samples/web-app-in-cluster-postgresql/src/gunicorn.conf.py Gunicorn worker signal handling configuration.
samples/web-app-in-cluster-postgresql/src/database.py PostgreSQL client helper used by the Flask app.
samples/web-app-in-cluster-postgresql/src/app.py Flask app wiring: routes for listing/adding/updating/deleting activities.
samples/web-app-in-cluster-postgresql/scripts/statefulset.yml In-cluster PostgreSQL Secret/ConfigMap/Services/StatefulSet with replication init scripts.
samples/web-app-in-cluster-postgresql/scripts/service.yml ClusterIP Service exposing the web app inside the cluster.
samples/web-app-in-cluster-postgresql/scripts/secret.yml App Secret template (PG password + Flask SECRET_KEY injected at deploy time).
samples/web-app-in-cluster-postgresql/scripts/namespace.yml Namespace manifest for the sample resources.
samples/web-app-in-cluster-postgresql/scripts/Dockerfile Multi-stage image build for the Flask app container.
samples/web-app-in-cluster-postgresql/scripts/deployment.yml Web app Deployment (env from ConfigMap/Secret, probes, resources).
samples/web-app-in-cluster-postgresql/scripts/configmap.yml App ConfigMap (PG host/port/db/user, login name, debug).
samples/web-app-in-cluster-postgresql/scripts/05-deploy-app.sh Deploys StatefulSet, provisions DB/user/schema, seeds data, then deploys app.
samples/web-app-in-cluster-postgresql/scripts/04-push-docker-image.sh Pushes the built image to ACR.
samples/web-app-in-cluster-postgresql/scripts/03-run-docker-container.sh Optional local smoke test via port-forward to in-cluster primary.
samples/web-app-in-cluster-postgresql/scripts/02-build-docker-image.sh Builds the app image from the src/ build context.
samples/web-app-in-cluster-postgresql/scripts/01-deploy-resources.sh Provisions RG + ACR only (no managed PostgreSQL).
samples/web-app-in-cluster-postgresql/scripts/00-variables.sh Central variables for resource names, DB topology, credentials, and k8s settings.
samples/web-app-in-cluster-postgresql/README.md Sample README rewritten for in-cluster PostgreSQL workflow.
README.md Adds the in-cluster PostgreSQL sample to the repo’s sample list.
Files not reviewed (4)
  • samples/web-app-in-cluster-postgresql/src/static/bootstrap/css/bootstrap-grid.min.css: Language not supported
  • samples/web-app-in-cluster-postgresql/src/static/bootstrap/css/bootstrap-grid.rtl.min.css: Language not supported
  • samples/web-app-in-cluster-postgresql/src/static/bootstrap/css/bootstrap-reboot.css: Language not supported
  • samples/web-app-in-cluster-postgresql/src/static/bootstrap/css/bootstrap-reboot.min.css: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +20 to +22
import psycopg2
from psycopg2.errors import OperationalError

Comment on lines +10 to +13
Connection is sourced from env vars: PG_HOST, PG_PORT, PG_USER, PG_PASSWORD, PG_DATABASE.
A retry loop is used on startup because the flex server can take a few seconds to become
reachable on the first deploy (especially under LocalStack where the postgres container is
spun up on first server creation).
Comment on lines +72 to +74
<td class="col-btn">
<form method="post" action="{{ url_for('delete', activity_id=loop.index0) }}" class="delete-form">
<button type="button" class="btn-delete" title="Delete activity" aria-label="Delete {{ activity[1] | e }}">
Comment on lines +81 to +86
@app.route("/delete/<int:activity_id>", methods=["POST"])
def delete(activity_id: int):
if 0 <= activity_id < len(activities):
db_client.delete_activity(activities[activity_id][0])
flash("Activity deleted.")
return redirect(url_for("index"))
Comment on lines +1 to +9
apiVersion: v1
kind: Secret
metadata:
name: pg-postgres-secret
namespace: vacation-planner-postgres
type: Opaque
stringData:
POSTGRES_PASSWORD: "SuperStrongPass123"
REPL_PASSWORD: "ReplStrongPass123"
Comment on lines +32 to +38
# Local port used by `kubectl port-forward` to reach the in-cluster DB from the
# host (scripts 03 and 06).
PG_LOCAL_PORT='5432'

# Application config — must match the seed-row `username` in 06-create-test-data.sh.
# PostgreSQL `=` is case-sensitive (unlike SQL Server), so this stays lowercase.
LOGIN_NAME='paolo'
Comment on lines +57 to +61
# The PostgreSQL database now runs in-cluster as a StatefulSet (statefulset.yml),
# deployed by 05-deploy-app.sh. Database provisioning and test data are handled by
# 06-create-test-data.sh. No Azure managed PostgreSQL resource is created here.
echo "Resource group and Azure Container Registry are ready."
echo "Next: build (02) and push (04) the image, deploy the app + in-cluster PostgreSQL (05), then seed data (06)."
Comment on lines +6 to +9
# The database runs in-cluster (statefulset.yml). Reach it from the host by
# port-forwarding the primary (write) Service to localhost:$PG_LOCAL_PORT.
# Requires 05-deploy-app.sh (deploys the DB) and 06-create-test-data.sh
# (creates PlannerDB + testuser) to have run first.
Comment on lines +220 to +222
accessModes: ["ReadWriteOnce"]
storageClassName: managed-csi-premium
resources:
Comment on lines +67 to +68
except (ConnectionError, ValueError) as e:
logger.error("Error writing activity: %s", e)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants