Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Every sample deploys the same Vacation Planner web app, a small Python [Flask](h

- [Azure SQL Database](https://learn.microsoft.com/en-us/azure/azure-sql/database/sql-database-paas-overview?view=azuresql)
- [Azure Database DB for PostgreSQL flexible server](https://learn.microsoft.com/en-us/azure/postgresql/overview)
- An in-cluster [PostgreSQL](https://www.postgresql.org/) database deployed as a Kubernetes [StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)
- [Azure Cosmos DB for MongoDB](https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/overview)
- [Azure Cosmos DB for NoSQL](https://learn.microsoft.com/en-us/azure/cosmos-db/overview)
- [Azure Blob Storage](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blobs-introduction)
Expand Down Expand Up @@ -69,6 +70,7 @@ All samples implement the same Vacation Planner web app. They only vary the unde
| ------ | ----------- |
| [web-app-sql-database](samples/web-app-sql-database/) | Stores activities in an [Azure SQL Database](https://learn.microsoft.com/en-us/azure/azure-sql/database/sql-database-paas-overview), connecting with a SQL login over TDS. |
| [web-app-postgresql-flexible-server](samples/web-app-postgresql-flexible-server/) | Stores activities in an [Azure Database for PostgreSQL flexible server](https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/overview). |
| [web-app-in-cluster-postgresql](samples/web-app-in-cluster-postgresql/) | Stores activities in an in-cluster [PostgreSQL](https://www.postgresql.org/) database deployed as a Kubernetes [StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/) (a primary with two streaming-replica standbys), instead of a managed Azure data service. |
| [web-app-cosmosdb-mongodb-api](samples/web-app-cosmosdb-mongodb-api/) | Stores activities in a collection of an [Azure Cosmos DB for MongoDB](https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/introduction) account. |
| [web-app-cosmosdb-nosql-api](samples/web-app-cosmosdb-nosql-api/) | Stores activities in a container of an [Azure Cosmos DB for NoSQL](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/) account. |
| [web-app-storage-account](samples/web-app-storage-account/) | Stores activities in an [Azure Blob Storage](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blobs-introduction) container, using a connection string. |
Expand Down
56 changes: 56 additions & 0 deletions samples/web-app-in-cluster-postgresql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Vacation Planner: in-cluster PostgreSQL

This sample demonstrates a Python Flask single-page web application called *Vacation Planner* hosted on an [Azure Kubernetes Service (AKS)](https://learn.microsoft.com/en-us/azure/aks/what-is-aks) cluster in the cloud on Azure or locally in the LocalStack emulator for Azure. The app runs in a dedicated namespace and stores activity data in the `activities` table of the `PlannerDB` database on an **in-cluster PostgreSQL database** — a primary plus two streaming-replica pods deployed as a Kubernetes [StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/), rather than a managed service such as Azure Database for PostgreSQL flexible server.

The database runs entirely inside the cluster: PostgreSQL 16 pods are backed by Azure managed-disk `PersistentVolumeClaim`s, and they are exposed through three `ClusterIP` services — a headless service for stable per-pod DNS, a *primary* (write) endpoint targeting the pod-0 leader, and a *read* endpoint that round-robins across all replicas. The application connects to the primary (write) endpoint using a dedicated application user (`testuser`) rather than the `postgres` superuser, and the deployment seeds the `activities` table with a handful of sample plans so the app shows data on first load.

Before installing the sample, make sure to create an [Azure Kubernetes Service (AKS)](https://learn.microsoft.com/en-us/azure/aks/what-is-aks) cluster by using one of the following scripts:

- [scripts/01-system-assigned-managed-identity.sh](../../scripts/01-system-assigned-managed-identity.sh): creates the cluster using a system-assigned managed identity as its cluster identity.
- [scripts/01-user-assigned-managed-identity.sh](../../scripts/01-user-assigned-managed-identity.sh): creates the cluster using a user-assigned managed identity as its cluster identity.

All commands below are run from this sample's `scripts/` folder.

## Deployment workflow

Run the numbered scripts in order from the `scripts/` folder:

```bash
cd scripts
./01-deploy-resources.sh
./02-build-docker-image.sh
./04-push-docker-image.sh
./05-deploy-app.sh
```

`05-deploy-app.sh` deploys the in-cluster PostgreSQL StatefulSet, provisions the database/user and seeds the sample data, and then deploys the app, so it requires `psql` on the host machine (it connects to the database through a `kubectl port-forward`).

Optionally, **after** `05-deploy-app.sh` has deployed and provisioned the database, run `./03-run-docker-container.sh` for a local smoke test — it runs the container outside Kubernetes and connects to the in-cluster database through a `kubectl port-forward`.

## Scripts and manifests

| File | Description |
| ---- | ----------- |
| [`00-variables.sh`](scripts/00-variables.sh) | Defines the variables shared across the other scripts (resource names, image tag, in-cluster PostgreSQL credentials, StatefulSet and service names, Kubernetes namespace, …). The other scripts load these values by sourcing this file. |
| [`01-deploy-resources.sh`](scripts/01-deploy-resources.sh) | Deploys the Azure resources used by this sample: the resource group and the [Azure Container Registry (ACR)](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-intro). There is no managed database to create — PostgreSQL runs in-cluster and is deployed by `05-deploy-app.sh`. |
| [`02-build-docker-image.sh`](scripts/02-build-docker-image.sh) | Builds the Docker image for the web app from the [`src/`](src/) folder. |
| [`03-run-docker-container.sh`](scripts/03-run-docker-container.sh) | Runs the web app in a local Docker container (no Kubernetes), connecting to the in-cluster database through a `kubectl port-forward` to the primary service. Run it after `05-deploy-app.sh` has deployed and provisioned the database. |
| [`04-push-docker-image.sh`](scripts/04-push-docker-image.sh) | Tags and pushes the Docker image to the Azure Container Registry, on Azure or in the LocalStack emulator. |
| [`05-deploy-app.sh`](scripts/05-deploy-app.sh) | Deploys the in-cluster PostgreSQL StatefulSet and waits for it to become ready, then (over a `kubectl port-forward` to the primary, so it requires `psql` on the host) creates the `PlannerDB` database, the dedicated application user and its grants, and the `activities` table, which it also seeds with sample data. Finally it deploys the app to the AKS cluster using the YAML manifests below (templated with `yq`). |
| [`Dockerfile`](scripts/Dockerfile) | Builds the Docker image of the web app. |
| [`namespace.yml`](scripts/namespace.yml) | Creates the Kubernetes namespace. |
| [`statefulset.yml`](scripts/statefulset.yml) | Creates the in-cluster PostgreSQL cluster: a Secret with the superuser and replication passwords, a ConfigMap with the primary/replica init scripts, the headless / primary (write) / read `ClusterIP` services, and a 3-replica StatefulSet (one primary plus two standbys configured for streaming replication) backed by Azure managed-disk PVCs. |
| [`configmap.yml`](scripts/configmap.yml) | Creates the ConfigMap holding non-secret input values (the in-cluster PostgreSQL primary service host, database, user, login name) passed to the app as environment variables. |
| [`secret.yml`](scripts/secret.yml) | Creates the Secret holding sensitive values (the application user's PostgreSQL password and the Flask secret key) passed to the app as environment variables. |
| [`deployment.yml`](scripts/deployment.yml) | Creates the Kubernetes Deployment, including the pod specification for the web app. |
| [`service.yml`](scripts/service.yml) | Creates the `ClusterIP` Service that exposes the web app inside the cluster. |

## Accessing the web app

The app is exposed through a `ClusterIP` service, which is only reachable from inside the cluster. Port-forward it to a local port to open it from your machine:

```bash
kubectl port-forward service/vacation-planner-postgres 8080:80 -n vacation-planner-postgres
```

Then browse to [http://localhost:8080](http://localhost:8080). Alternatively, use a tool such as [k9s](https://k9scli.io/) to start the port-forward interactively.
51 changes: 51 additions & 0 deletions samples/web-app-in-cluster-postgresql/scripts/00-variables.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Variables

# Azure Resources
PREFIX='zeus'
SUFFIX='test'
LOCATION='italynorth'
RESOURCE_GROUP_NAME="${PREFIX}-rg"
ACR_NAME="${PREFIX,,}acr${SUFFIX,,}"
ACR_SKU='Standard'
SUBSCRIPTION_NAME=$(az account show --query name --output tsv)
SUBSCRIPTION_ID=$(az account show --query id --output tsv)
TENANT_ID=$(az account show --query tenantId --output tsv)
CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)"

# In-cluster PostgreSQL (deployed via statefulset.yml)
PG_PORT='5432'
PG_USER_NAME='testuser'
PG_USER_PASSWORD='TestP@ssw0rd123'
PG_DATABASE_NAME='PlannerDB'

# StatefulSet topology and write/read endpoints (see statefulset.yml).
# The app does writes, so it must target the primary (write) endpoint.
PG_STATEFULSET_NAME='pg-postgres'
PG_PRIMARY_POD='pg-postgres-0'
PG_PRIMARY_SERVICE='pg-postgres-primary'

# Superuser bootstrap credentials. These MUST match the POSTGRES_PASSWORD in the
# pg-postgres-secret defined in statefulset.yml — keep both in sync if changed.
PG_SUPERUSER='postgres'
PG_SUPERUSER_PASSWORD='SuperStrongPass123'

Comment on lines +15 to +31
# 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 +32 to +38

# Docker Image
IMAGE_NAME="vacation-planner-postgres"
IMAGE_PULL_POLICY="Always"
IMAGE_TAG="v1"
PORT="8080"

# Kubernetes
NAMESPACE="vacation-planner-postgres"
DEPLOYMENT_NAME="vacation-planner-postgres"
SERVICE_NAME="vacation-planner-postgres"
CONFIGMAP_NAME="vacation-planner-postgres-config"
K8S_SECRET_NAME="vacation-planner-postgres-secrets"
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/bin/bash

# Variables
source ./00-variables.sh

# Change the current directory to the script's directory
cd "$CURRENT_DIR" || exit

# Create a resource group
echo "Checking if resource group [$RESOURCE_GROUP_NAME] exists in the subscription [$SUBSCRIPTION_NAME]..."
az group show --name $RESOURCE_GROUP_NAME &>/dev/null

if [[ $? != 0 ]]; then
echo "Creating resource group [$RESOURCE_GROUP_NAME]..."
az group create \
--name $RESOURCE_GROUP_NAME \
--location "$LOCATION" \
--only-show-errors 1>/dev/null

if [[ $? == 0 ]]; then
echo "Resource group [$RESOURCE_GROUP_NAME] created."
else
echo "Failed to create resource group [$RESOURCE_GROUP_NAME]."
exit 1
fi
else
echo "Resource group [$RESOURCE_GROUP_NAME] already exists."
fi

# Create the Azure Container Registry
echo "Checking if [$ACR_NAME] Azure Container Registry exists..."
az acr show \
--name "$ACR_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--only-show-errors &>/dev/null

if [[ $? != 0 ]]; then
echo "Creating Azure Container Registry [$ACR_NAME]..."
az acr create \
--name "$ACR_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--location "$LOCATION" \
--sku "$ACR_SKU" \
--admin-enabled "true" \
--only-show-errors 1>/dev/null

if [ $? -eq 0 ]; then
echo "Azure Container Registry [$ACR_NAME] created."
else
echo "Failed to create Azure Container Registry [$ACR_NAME]."
exit 1
fi
else
echo "[$ACR_NAME] Azure Container Registry already exists."
fi

# 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 +57 to +61
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash

# Variables
source ./00-variables.sh

# Change the current directory to the script's directory
cd "$CURRENT_DIR" || exit

# Build context: the src/ folder (contains app.py, database.py, requirements.txt,
# gunicorn.conf.py, static/, templates/).
# The Dockerfile lives alongside this script, so we point -f at it explicitly.
BUILD_CONTEXT="../src"

# Build the docker image
docker build \
-t $IMAGE_NAME:$IMAGE_TAG \
-f Dockerfile \
--build-arg PORT=$PORT \
$BUILD_CONTEXT
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/bash

# Variables
source ./00-variables.sh

# 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 +6 to +9
echo "Port-forwarding svc/$PG_PRIMARY_SERVICE to localhost:$PG_LOCAL_PORT..."
kubectl port-forward -n "$NAMESPACE" "svc/$PG_PRIMARY_SERVICE" "$PG_LOCAL_PORT:5432" &
PF_PID=$!
trap 'kill "$PF_PID" 2>/dev/null' EXIT

# Wait for the forwarded port to accept connections.
echo "Waiting for PostgreSQL to accept connections on localhost:$PG_LOCAL_PORT..."
until pg_isready -h localhost -p "$PG_LOCAL_PORT" -U "$PG_USER_NAME" &>/dev/null; do
sleep 2
done

# --network=host so the container reaches the port-forward on the host's loopback.
docker run -it \
--rm \
--network=host \
-e PORT=$PORT \
-e PG_HOST="localhost" \
-e PG_PORT="$PG_LOCAL_PORT" \
-e PG_DATABASE="$PG_DATABASE_NAME" \
-e PG_USER="$PG_USER_NAME" \
-e PG_PASSWORD="$PG_USER_PASSWORD" \
-e LOGIN_NAME="$LOGIN_NAME" \
--name "$IMAGE_NAME" \
"$IMAGE_NAME:$IMAGE_TAG"
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/bash

# Variables
source ./00-variables.sh

# Login to ACR
echo "Logging into Azure Container Registry [$ACR_NAME]..."
az acr login --name $ACR_NAME

# Retrieve ACR login server. Each container image needs to be tagged with the loginServer name of the registry.
ACR_LOGIN_SERVER=$(az acr show --name $ACR_NAME --query loginServer --output tsv)

if [ $? -eq 0 ]; then
echo "Logged into Azure Container Registry [$ACR_NAME] successfully."
else
echo "Failed to log into Azure Container Registry [$ACR_NAME]."
exit 1
fi

FULL_IMAGE="${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG}"

# Tag the local image with the loginServer of ACR
docker tag ${IMAGE_NAME,,}:$IMAGE_TAG $ACR_LOGIN_SERVER/${IMAGE_NAME,,}:$IMAGE_TAG

if [ $? -eq 0 ]; then
echo "Docker image [$IMAGE_NAME] tagged as [$FULL_IMAGE] successfully."
else
echo "Failed to tag Docker image [$IMAGE_NAME] as [$FULL_IMAGE]."
exit 1
fi

# Push the container image to ACR
docker push $ACR_LOGIN_SERVER/${IMAGE_NAME,,}:$IMAGE_TAG

if [ $? -eq 0 ]; then
echo "Docker image [$FULL_IMAGE] pushed to ACR successfully."
else
echo "Failed to push Docker image [$FULL_IMAGE] to ACR."
exit 1
fi
Loading