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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ All samples implement the same Vacation Planner web app. They only vary the unde
| Sample | Description |
| ------ | ----------- |
| [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-mysql-flexible-server](samples/web-app-mysql-flexible-server/) | Stores activities in an [Azure Database for MySQL flexible server](https://learn.microsoft.com/en-us/azure/mysql/flexible-server/overview). |
| [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. |
Expand Down
54 changes: 54 additions & 0 deletions samples/web-app-mysql-flexible-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Vacation Planner: Azure Database for MySQL flexible server

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 [Azure Database for MySQL flexible server](https://learn.microsoft.com/en-us/azure/mysql/flexible-server/overview).

The application connects to MySQL using a dedicated application user (rather than the server admin) over TLS, and the deployment scripts seed 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.

The `01-deploy-resources.sh` script needs the `mysql` client installed on the host (for example `sudo apt install -y mysql-client`) to bootstrap the application user, schema, and seed data.

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
./03-run-docker-container.sh # optional local smoke test
./04-push-docker-image.sh
./05-deploy-app.sh
```

## Scripts and manifests

| File | Description |
| ---- | ----------- |
| [`00-variables.sh`](scripts/00-variables.sh) | Defines the variables shared across the other scripts (resource names, image tag, MySQL credentials, 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, the [Azure Container Registry (ACR)](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-intro), the [Azure Database for MySQL flexible server](https://learn.microsoft.com/en-us/azure/mysql/flexible-server/overview) and the `plannerdb` database, a permissive firewall rule (dev/test only), a dedicated application user, and the `activities` table, which it also seeds with sample data. Requires the `mysql` client on the host. |
| [`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) to validate that it starts and connects to the database as expected. |
| [`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) | Uses the YAML manifests below (templated with `yq`) to deploy the app to the AKS cluster. |
| [`Dockerfile`](scripts/Dockerfile) | Builds the Docker image of the web app. |
| [`namespace.yml`](scripts/namespace.yml) | Creates the Kubernetes namespace. |
| [`configmap.yml`](scripts/configmap.yml) | Creates the ConfigMap holding non-secret input values (MySQL host, port, database, user, TLS flag, login name) passed to the app as environment variables. |
| [`secret.yml`](scripts/secret.yml) | Creates the Secret holding sensitive values (the MySQL 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-mysql 8080:80 -n vacation-planner-mysql
```

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.
48 changes: 48 additions & 0 deletions samples/web-app-mysql-flexible-server/scripts/00-variables.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Variables

# Azure Resources
PREFIX='local'
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)"

# Azure Database for MySQL flexible server
MYSQL_SERVER_NAME="${PREFIX}-mysqlflex-${SUFFIX}"
MYSQL_VERSION='8.0.21'
MYSQL_SKU_TIER='Burstable'
MYSQL_SKU_NAME='Standard_B1ms'
MYSQL_STORAGE_SIZE_GB=32
MYSQL_BACKUP_RETENTION_DAYS=7
MYSQL_PORT='3306'
FIREWALL_RULE_NAME='AllowAllIPs'
MYSQL_ADMIN_USER='myadmin'
MYSQL_ADMIN_PASSWORD='P@ssw0rd1234!'
MYSQL_USER_NAME='testuser'
MYSQL_USER_PASSWORD='TestP@ssw0rd123'
MYSQL_DATABASE_NAME='plannerdb'
# Azure MySQL Flexible Server (and the LocalStack emulator) default require_secure_transport=ON,
# so the app must connect over TLS. The app enables TLS without certificate verification when
# MYSQL_SSL is truthy, which works against both LocalStack (self-signed cert) and real Azure.
MYSQL_SSL='true'

# Application config — must match the seed-row `username` in 01-deploy-resources.sh.
LOGIN_NAME='paolo'

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

# Kubernetes
NAMESPACE="vacation-planner-mysql"
DEPLOYMENT_NAME="vacation-planner-mysql"
SERVICE_NAME="vacation-planner-mysql"
CONFIGMAP_NAME="vacation-planner-mysql-config"
K8S_SECRET_NAME="vacation-planner-mysql-secrets"
273 changes: 273 additions & 0 deletions samples/web-app-mysql-flexible-server/scripts/01-deploy-resources.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
#!/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

# Create the Azure Database for MySQL flexible server
echo "Checking if MySQL flexible server [$MYSQL_SERVER_NAME] exists..."
az mysql flexible-server show \
--name "$MYSQL_SERVER_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--only-show-errors &>/dev/null

if [[ $? != 0 ]]; then
echo "Creating MySQL flexible server [$MYSQL_SERVER_NAME]..."
az mysql flexible-server create \
--name "$MYSQL_SERVER_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--location "$LOCATION" \
--tier "$MYSQL_SKU_TIER" \
--sku-name "$MYSQL_SKU_NAME" \
--version "$MYSQL_VERSION" \
--storage-size "$MYSQL_STORAGE_SIZE_GB" \
--backup-retention "$MYSQL_BACKUP_RETENTION_DAYS" \
--geo-redundant-backup Disabled \
--admin-user "$MYSQL_ADMIN_USER" \
--admin-password "$MYSQL_ADMIN_PASSWORD" \
--public-access Enabled \
--high-availability Disabled \
--yes \
--only-show-errors 1>/dev/null

if [ $? -eq 0 ]; then
echo "MySQL flexible server [$MYSQL_SERVER_NAME] created."
else
echo "Failed to create MySQL flexible server [$MYSQL_SERVER_NAME]."
exit 1
fi
else
echo "MySQL flexible server [$MYSQL_SERVER_NAME] already exists."
fi

# Add a permissive firewall rule (dev/test only)
echo "Ensuring firewall rule [$FIREWALL_RULE_NAME] exists on MySQL flexible server [$MYSQL_SERVER_NAME]..."
az mysql flexible-server firewall-rule create \
--resource-group "$RESOURCE_GROUP_NAME" \
--name "$MYSQL_SERVER_NAME" \
--rule-name "$FIREWALL_RULE_NAME" \
--start-ip-address 0.0.0.0 \
--end-ip-address 255.255.255.255 \
--only-show-errors 1>/dev/null

# Create the MySQL database
echo "Checking if MySQL database [$MYSQL_DATABASE_NAME] exists..."
az mysql flexible-server db show \
--database-name "$MYSQL_DATABASE_NAME" \
--server-name "$MYSQL_SERVER_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--only-show-errors &>/dev/null

if [[ $? != 0 ]]; then
echo "Creating MySQL database [$MYSQL_DATABASE_NAME]..."
az mysql flexible-server db create \
--database-name "$MYSQL_DATABASE_NAME" \
--server-name "$MYSQL_SERVER_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--charset utf8mb4 \
--collation utf8mb4_unicode_ci \
--only-show-errors 1>/dev/null

if [ $? -eq 0 ]; then
echo "MySQL database [$MYSQL_DATABASE_NAME] created."
else
echo "Failed to create MySQL database [$MYSQL_DATABASE_NAME]."
exit 1
fi
else
echo "MySQL database [$MYSQL_DATABASE_NAME] already exists."
fi

# Retrieve MySQL server FQDN
MYSQL_FQDN_FULL=$(az mysql flexible-server show \
--name "$MYSQL_SERVER_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--query "fullyQualifiedDomainName" \
--output tsv)

if [ -z "$MYSQL_FQDN_FULL" ]; then
echo "Failed to retrieve MySQL server FQDN."
exit 1
fi

# Split host:port — the LocalStack emulator embeds the dynamically allocated TCP-proxy port
# directly in fullyQualifiedDomainName, mirroring the storage / container registry emulators.
# Real Azure returns just the bare host so MYSQL_PORT stays at the value from 00-variables.sh (3306).
MYSQL_FQDN="${MYSQL_FQDN_FULL%%:*}"
if [[ "$MYSQL_FQDN_FULL" == *:* ]]; then
MYSQL_PORT="${MYSQL_FQDN_FULL##*:}"
fi
echo "MySQL host = $MYSQL_FQDN, port = $MYSQL_PORT"

# The mysql client must be available on the host machine for the bootstrap below.
if ! command -v mysql &>/dev/null; then
echo "mysql is not installed on the host. Install the MySQL client (mysql-client) and re-run." >&2
exit 1
fi

# Wait for the MySQL flexible server to accept connections.
# --ssl-mode=REQUIRED: Azure (and the LocalStack emulator) enforce require_secure_transport=ON,
# so every connection must negotiate TLS.
echo "Waiting for the [$MYSQL_SERVER_NAME] MySQL flexible server to accept connections..."
MYSQL_READY=0
for attempt in $(seq 1 30); do
if MYSQL_PWD="$MYSQL_ADMIN_PASSWORD" mysql \
--host="$MYSQL_FQDN" \
--port="$MYSQL_PORT" \
--user="$MYSQL_ADMIN_USER" \
--protocol=TCP \
--ssl-mode=REQUIRED \
--connect-timeout=5 \
-e "SELECT 1;" &>/dev/null; then
MYSQL_READY=1
echo "MySQL flexible server is accepting connections (attempt $attempt/30)"
break
fi
echo "MySQL flexible server not ready yet (attempt $attempt/30)..."
sleep 2
done

if [ "$MYSQL_READY" -ne 1 ]; then
echo "MySQL flexible server did not become reachable after 30 attempts. Exiting."
exit 1
fi

# Create the application user [$MYSQL_USER_NAME] and grant it access to the database
echo "Creating login [$MYSQL_USER_NAME] on the [$MYSQL_SERVER_NAME] MySQL flexible server..."
MYSQL_PWD="$MYSQL_ADMIN_PASSWORD" mysql \
--host="$MYSQL_FQDN" \
--port="$MYSQL_PORT" \
--user="$MYSQL_ADMIN_USER" \
--protocol=TCP \
--ssl-mode=REQUIRED \
-e "CREATE USER IF NOT EXISTS '$MYSQL_USER_NAME'@'%' IDENTIFIED BY '$MYSQL_USER_PASSWORD';
GRANT ALL PRIVILEGES ON \`$MYSQL_DATABASE_NAME\`.* TO '$MYSQL_USER_NAME'@'%';
FLUSH PRIVILEGES;"

if [ $? -eq 0 ]; then
echo "Login [$MYSQL_USER_NAME] created successfully"
else
echo "Failed to create login [$MYSQL_USER_NAME]"
exit 1
fi

# Create [activities] table
echo "Creating [activities] table in the [$MYSQL_DATABASE_NAME] database..."
MYSQL_PWD="$MYSQL_USER_PASSWORD" mysql \
--host="$MYSQL_FQDN" \
--port="$MYSQL_PORT" \
--user="$MYSQL_USER_NAME" \
--protocol=TCP \
--ssl-mode=REQUIRED \
--database="$MYSQL_DATABASE_NAME" \
-e "CREATE TABLE IF NOT EXISTS activities (
id VARCHAR(32) NOT NULL,
username VARCHAR(255) NOT NULL,
activity TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
INDEX idx_activities_username (username),
INDEX idx_activities_created_at (created_at DESC)
);"

if [ $? -eq 0 ]; then
echo "[activities] table created successfully"
else
echo "Failed to create [activities] table"
exit 1
fi

# Insert sample data
echo "Inserting sample data into [activities] table..."
MYSQL_PWD="$MYSQL_USER_PASSWORD" mysql \
--host="$MYSQL_FQDN" \
--port="$MYSQL_PORT" \
--user="$MYSQL_USER_NAME" \
--protocol=TCP \
--ssl-mode=REQUIRED \
--database="$MYSQL_DATABASE_NAME" \
-e "INSERT IGNORE INTO activities (id, username, activity) VALUES
(MD5('paolo_pisa_seed'), 'paolo', 'Visit the Leaning Tower in Pisa'),
(MD5('paolo_volterra_seed'), 'paolo', 'Explore Etruscan walls in Volterra'),
(MD5('paolo_san_gimignano_seed'), 'paolo', 'Climb Torre Grossa in San Gimignano'),
(MD5('paolo_siena_seed'), 'paolo', 'Walk across Piazza del Campo in Siena'),
(MD5('paolo_montalcino_seed'), 'paolo', 'Taste Brunello wine in Montalcino'),
(MD5('paolo_pienza_seed'), 'paolo', 'Sample Pecorino cheese in Pienza'),
(MD5('paolo_florence_seed'), 'paolo', 'Admire Michelangelo''s David in Florence'),
(MD5('paolo_viareggio_beach_seed'), 'paolo', 'Relax by the beach in Viareggio'),
(MD5('paolo_viareggio_promenade_seed'), 'paolo', 'Stroll along the Viareggio promenade');"

if [ $? -eq 0 ]; then
echo "Test data inserted successfully into [activities] table"
else
echo "Failed to insert test data into [activities] table"
exit 1
fi

# Query data
echo "Querying test data from [activities] table..."
MYSQL_PWD="$MYSQL_USER_PASSWORD" mysql \
--host="$MYSQL_FQDN" \
--port="$MYSQL_PORT" \
--user="$MYSQL_USER_NAME" \
--protocol=TCP \
--ssl-mode=REQUIRED \
--database="$MYSQL_DATABASE_NAME" \
-e "SELECT id, username, activity, created_at FROM activities;"

if [ $? -eq 0 ]; then
echo "Test data queried successfully from [activities] table"
else
echo "Failed to query test data from [activities] table"
exit 1
fi
Loading