From e4ab130b1f2e6688c149a844145e15826d505736 Mon Sep 17 00:00:00 2001 From: Dylan de Jong Date: Wed, 21 Oct 2026 17:48:39 +0200 Subject: [PATCH 1/6] Scope Azure PostgreSQL server names to resource group (recipe 1.0.7) Hash resourceGroup().id with the Radius resource ID so workshops reusing identical Radius IDs no longer collide on globally unique server names. Publish as immutable 1.0.7, never overwrite existing tags, add a validation guard, and document upgrade, ServerNameAlreadyExists troubleshooting, and replacement risk. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../challenges/challenge-05.md | 11 +- .../challenges/challenge-06.md | 3 + .../iac/recipes/postgres-azure-flex.bicep | 6 +- .../resources/configure-recipes.sh | 33 ++++-- .../resources/validate-postgres-recipes.sh | 7 ++ .../tests/test_console_helpers.py | 2 +- .../walkthrough/challenge-05/solution-05.md | 101 +++++++++++++++--- .../walkthrough/challenge-06/solution-06.md | 22 ++++ .../walkthrough/challenge-07/solution-07.md | 13 +++ .../walkthrough/challenge-09/solution-09.md | 4 +- 10 files changed, 169 insertions(+), 33 deletions(-) diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/challenges/challenge-05.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/challenges/challenge-05.md index cbf3fbf4f..89a8baddb 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/challenges/challenge-05.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/challenges/challenge-05.md @@ -64,7 +64,7 @@ While targeting AKS and workspace `ws-azure-prod`: recreating it. BYOS: create or select a globally unique, lowercase registry. 2. Authenticate to the registry. 3. Publish `iac/recipes/sql-server.bicep` and the workshop-owned Azure and Kubernetes - PostgreSQL recipes as version `1.0.0`. + PostgreSQL recipes as version `1.0.7`. Never overwrite an existing version tag. 4. Register it as the default recipe for `Radius.Resources/sqlDatabases` in `env-azure-prod`. 5. Verify that the registration refers to the expected OCI artifact. @@ -120,9 +120,11 @@ Prepare a comparison that answers: ## Success criteria -- The custom SQL recipe is published to ACR at version `1.0.0`. -- Both corrected PostgreSQL recipes are published to ACR at version `1.0.0` and +- The custom SQL recipe is published to ACR at version `1.0.7`. +- Both corrected PostgreSQL recipes are published to ACR at version `1.0.7` and registered without mutable `latest` references. +- The Azure PostgreSQL server name is deterministic and scoped to both the workshop's + Azure resource group and the Radius resource ID. - Azure PostgreSQL permits only the exact static AKS egress IPs. - The recipe is registered as the default `Radius.Resources/sqlDatabases` implementation in `env-azure-prod`. @@ -140,6 +142,9 @@ Prepare a comparison that answers: - Publishing an artifact and registering a recipe are separate steps. Validate each one before continuing. - ACR names must be globally unique and lowercase. +- Azure PostgreSQL server names are globally unique too. Separate workshops can share + Radius resource IDs, so give each workshop its own Azure resource group and include + `resourceGroup().id` in the deterministic name. Never use a timestamp. - Check Kubernetes context, Radius workspace, group, and environment before each deployment. - The Azure environment needs an Azure provider scope; the K3s environment does not. diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/challenges/challenge-06.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/challenges/challenge-06.md index 39c2e6fb0..d7b3218ba 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/challenges/challenge-06.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/challenges/challenge-06.md @@ -151,6 +151,9 @@ Capture a side-by-side comparison that shows: 1. Radius installation alone does not make custom resource types deployable. 2. The application needs the extension package generated from the Challenge 04 catalog. 3. A `RecipeNotFoundFailure` is an environment-layer problem. Return to Challenge 05. +4. `ServerNameAlreadyExists` for PostgreSQL is also an environment-layer problem. Use a + workshop-specific Azure resource group, register Azure PostgreSQL recipe `1.0.7`, + then retry the unchanged deployment. ### Deployment and validation diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/iac/recipes/postgres-azure-flex.bicep b/03-Azure/01-01-App Innovation/04-adaptive-apps/iac/recipes/postgres-azure-flex.bicep index 4a2e4d7e1..1c5792dd9 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/iac/recipes/postgres-azure-flex.bicep +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/iac/recipes/postgres-azure-flex.bicep @@ -37,7 +37,11 @@ var skuBySize = { tier: 'GeneralPurpose' } } -var serverName = 'pg-${uniqueString(context.resource.id)}' +// Azure PostgreSQL server names are globally unique, while separate workshops commonly reuse +// the same Radius resource ID. Include the target resource group ID, which also contains the +// subscription ID, so isolated workshops do not collide. Every input is stable, so retries reuse +// the same name. Changing these inputs creates a different server rather than renaming one. +var serverName = 'pg-${uniqueString(resourceGroup().id, context.resource.id)}' var egressIps = map(filter(split(aksEgressIps, ','), ip => !empty(trim(ip))), ip => trim(ip)) // Strip carriage returns from everything that is executed or interpreted inside the cluster. // Both files are stored in git with LF, but a Windows checkout rewrites them to CRLF unless diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/configure-recipes.sh b/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/configure-recipes.sh index e93a62eca..c99fea076 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/configure-recipes.sh +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/configure-recipes.sh @@ -27,7 +27,7 @@ AKS_ENVIRONMENT="${AKS_ENVIRONMENT:-env-azure-prod}" K3S_ENVIRONMENT="${K3S_ENVIRONMENT:-env-local-prod}" RADIUS_GROUP="${RADIUS_GROUP:-rg-trading}" RECIPE_REGISTRY="${RECIPE_REGISTRY:-ghcr.io/microsoft/adaptive-apps/recipes}" -WORKSHOP_RECIPE_VERSION="${WORKSHOP_RECIPE_VERSION:-1.0.6}" +WORKSHOP_RECIPE_VERSION="${WORKSHOP_RECIPE_VERSION:-1.0.7}" [[ "$WORKSHOP_RECIPE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "WORKSHOP_RECIPE_VERSION must be an immutable semantic version such as 1.0.0." >&2 exit 1 @@ -158,15 +158,28 @@ publish_workshop_recipes() { } EOF - DOCKER_CONFIG="${TEMP_DIR}/docker" rad bicep publish \ - --file iac/recipes/sql-server.bicep \ - --target "br:${ACR_NAME}.azurecr.io/recipes/sql-server:${WORKSHOP_RECIPE_VERSION}" - DOCKER_CONFIG="${TEMP_DIR}/docker" rad bicep publish \ - --file iac/recipes/postgres-azure-flex.bicep \ - --target "br:${ACR_NAME}.azurecr.io/recipes/postgres-azure-flex:${WORKSHOP_RECIPE_VERSION}" - DOCKER_CONFIG="${TEMP_DIR}/docker" rad bicep publish \ - --file iac/recipes/postgres-kubernetes.bicep \ - --target "br:${ACR_NAME}.azurecr.io/recipes/postgres-kubernetes:${WORKSHOP_RECIPE_VERSION}" + publish_recipe() { + local file="$1" + local repository="$2" + local image="${repository}:${WORKSHOP_RECIPE_VERSION}" + + # Version tags are immutable workshop artifacts. Republishing changed source under an + # existing tag would silently alter every environment already registered to that tag. + if az acr repository show \ + --name "$ACR_NAME" \ + --image "$image" >/dev/null 2>&1; then + echo "Reusing existing ${ACR_NAME}.azurecr.io/${image}; published versions are not overwritten." + return 0 + fi + + DOCKER_CONFIG="${TEMP_DIR}/docker" rad bicep publish \ + --file "$file" \ + --target "br:${ACR_NAME}.azurecr.io/${image}" + } + + publish_recipe iac/recipes/sql-server.bicep recipes/sql-server + publish_recipe iac/recipes/postgres-azure-flex.bicep recipes/postgres-azure-flex + publish_recipe iac/recipes/postgres-kubernetes.bicep recipes/postgres-kubernetes } assert_recipes() { diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/validate-postgres-recipes.sh b/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/validate-postgres-recipes.sh index 2eb228e14..21513a05f 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/validate-postgres-recipes.sh +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/validate-postgres-recipes.sh @@ -62,6 +62,13 @@ for recipe in "$AZURE_RECIPE" "$SQL_RECIPE"; do done done +# Azure PostgreSQL server names are globally unique. Workshops often reuse Radius resource IDs, +# so the deterministic name must also be scoped to the target Azure subscription/resource group. +grep -Fqx "var serverName = 'pg-\${uniqueString(resourceGroup().id, context.resource.id)}'" "$AZURE_RECIPE" || { + echo "Azure PostgreSQL server name must hash resourceGroup().id and context.resource.id." >&2 + exit 1 +} + # sql-server.bicep provisions only Azure resources, so any 'radapp.io/' key in it is an # invalid Azure tag name rather than a Kubernetes label. if grep -Fq "radapp.io/" "$SQL_RECIPE"; then diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/tests/test_console_helpers.py b/03-Azure/01-01-App Innovation/04-adaptive-apps/tests/test_console_helpers.py index 6f41422ce..e7c1c77fe 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/tests/test_console_helpers.py +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/tests/test_console_helpers.py @@ -114,7 +114,7 @@ def kubeconfig(name): "aiModels", "governance", "agentGuardrails", "sqlDatabases"] recipes = [{"name": os.environ.get("RECIPE_NAME", "default"), "resourceType": "Radius.Resources/" + kind, "templateKind": "bicep", - "templatePath": "mock.azurecr.io/recipe:1.0.6"} for kind in types] + "templatePath": "mock.azurecr.io/recipe:1.0.7"} for kind in types] print(os.environ.get("RECIPE_JSON", json.dumps(recipes))) elif args[:2] == ["bicep", "publish-extension"]: Path(arg("--target")).write_text("mock-extension") diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md index 3591f5687..02e769825 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md @@ -152,6 +152,29 @@ kubectl get jobs -n `COMPLETIONS` must read `1/1`. +### Globally unique Azure PostgreSQL names + +Azure Database for PostgreSQL Flexible Server names are globally unique. Separate +workshops commonly use the same Radius resource ID, for example the same group, +application, and `trading-db` resource names on different clusters. Recipe version +`1.0.7` therefore scopes the deterministic server name to the target Azure resource group: + +```bicep +var serverName = 'pg-${uniqueString(resourceGroup().id, context.resource.id)}' +``` + +`resourceGroup().id` contains both the subscription ID and resource-group name, so +identical Radius resource IDs in different Azure resource groups receive different +names. Different Radius resources in one resource group also receive different names, +and retries with the same inputs reuse the same name. The result is `pg-` plus 13 +lowercase alphanumeric characters, which satisfies the Flexible Server naming rules. + +Give every workshop its own Azure resource group in the AKS environment's Azure +provider scope. Hosted Console labs already do this. Sharing an Azure resource group +between workshops is not supported by this recipe; a design that needs it must add a +stable, explicit workshop identifier to the hash. Never use a timestamp or another +value that changes on every deployment. + ## Manual tutorial, Stage 1: Author the Azure SQL recipe Open `iac/recipes/sql-server.bicep` and walk through: @@ -306,20 +329,22 @@ EOF Publish the teaching recipe and both workshop-owned PostgreSQL recipes. The PostgreSQL recipes compile the same `iac/recipes/trading-schema.sql` into each implementation and -use immutable workshop tags rather than the external `latest` recipes. +use immutable workshop tags rather than the external `latest` recipes. Version `1.0.7` +contains the Azure PostgreSQL naming correction. Do not republish changed source over +`1.0.6` or another existing tag; publish a new version instead. ```bash rad bicep publish \ --file iac/recipes/sql-server.bicep \ - --target "br:${ACR_NAME}.azurecr.io/recipes/sql-server:1.0.6" + --target "br:${ACR_NAME}.azurecr.io/recipes/sql-server:1.0.7" rad bicep publish \ --file iac/recipes/postgres-azure-flex.bicep \ - --target "br:${ACR_NAME}.azurecr.io/recipes/postgres-azure-flex:1.0.6" + --target "br:${ACR_NAME}.azurecr.io/recipes/postgres-azure-flex:1.0.7" rad bicep publish \ --file iac/recipes/postgres-kubernetes.bicep \ - --target "br:${ACR_NAME}.azurecr.io/recipes/postgres-kubernetes:1.0.6" + --target "br:${ACR_NAME}.azurecr.io/recipes/postgres-kubernetes:1.0.7" ``` **PowerShell 7:** @@ -327,15 +352,15 @@ rad bicep publish \ ```powershell rad bicep publish ` --file iac/recipes/sql-server.bicep ` - --target "br:$env:ACR_NAME.azurecr.io/recipes/sql-server:1.0.6" + --target "br:$env:ACR_NAME.azurecr.io/recipes/sql-server:1.0.7" rad bicep publish ` --file iac/recipes/postgres-azure-flex.bicep ` - --target "br:$env:ACR_NAME.azurecr.io/recipes/postgres-azure-flex:1.0.6" + --target "br:$env:ACR_NAME.azurecr.io/recipes/postgres-azure-flex:1.0.7" rad bicep publish ` --file iac/recipes/postgres-kubernetes.bicep ` - --target "br:$env:ACR_NAME.azurecr.io/recipes/postgres-kubernetes:1.0.6" + --target "br:$env:ACR_NAME.azurecr.io/recipes/postgres-kubernetes:1.0.7" ``` Register the Azure SQL teaching recipe: @@ -345,7 +370,7 @@ rad recipe register default \ --environment env-azure-prod \ --resource-type Radius.Resources/sqlDatabases \ --template-kind bicep \ - --template-path "${ACR_NAME}.azurecr.io/recipes/sql-server:1.0.6" + --template-path "${ACR_NAME}.azurecr.io/recipes/sql-server:1.0.7" ``` Verify: @@ -424,9 +449,9 @@ rad deploy iac/aks-env.bicep \ --parameters azureSubscriptionId="$AZURE_SUBSCRIPTION" \ --parameters azureResourceGroup="$RESOURCE_GROUP" \ --parameters istioRevision="$ISTIO_REVISION" \ - --parameters postgresRecipeTemplatePath="${ACR_NAME}.azurecr.io/recipes/postgres-azure-flex:1.0.6" \ + --parameters postgresRecipeTemplatePath="${ACR_NAME}.azurecr.io/recipes/postgres-azure-flex:1.0.7" \ --parameters aksEgressIps="$AKS_EGRESS_IPS" \ - --parameters sqlDatabasesRecipeTemplatePath="${ACR_NAME}.azurecr.io/recipes/sql-server:1.0.6" + --parameters sqlDatabasesRecipeTemplatePath="${ACR_NAME}.azurecr.io/recipes/sql-server:1.0.7" ``` **PowerShell 7:** @@ -477,9 +502,9 @@ rad deploy iac/aks-env.bicep ` --parameters azureSubscriptionId=$env:AZURE_SUBSCRIPTION ` --parameters azureResourceGroup=$env:RESOURCE_GROUP ` --parameters istioRevision=$IstioRevision ` - --parameters "postgresRecipeTemplatePath=$env:ACR_NAME.azurecr.io/recipes/postgres-azure-flex:1.0.6" ` + --parameters "postgresRecipeTemplatePath=$env:ACR_NAME.azurecr.io/recipes/postgres-azure-flex:1.0.7" ` --parameters "aksEgressIps=$($EgressIps -join ',')" ` - --parameters "sqlDatabasesRecipeTemplatePath=$env:ACR_NAME.azurecr.io/recipes/sql-server:1.0.6" + --parameters "sqlDatabasesRecipeTemplatePath=$env:ACR_NAME.azurecr.io/recipes/sql-server:1.0.7" ``` | Resource type | AKS/Azure backend | @@ -501,6 +526,47 @@ rad deploy iac/aks-env.bicep ` > [Why AKS uses Mosquitto for `mqttBrokers`](https://github.com/microsoft/MicroHack/blob/main/03-Azure/01-01-App%20Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md#why-aks-uses-mosquitto-for-mqttbrokers), > including the `mqttRecipeTemplatePath` override that restores the Event Grid recipe. +### Update an existing AKS environment to recipe `1.0.7` + +Changing the naming algorithm changes the Azure resource name. It is not an in-place +rename. Decide from the current `trading-db` state before changing the registration: + +```bash +rad recipe list \ + --workspace ws-azure-prod \ + --group rg-trading \ + --environment env-azure-prod +rad resource show Radius.Resources/postgreSqlDatabases trading-db \ + --workspace ws-azure-prod \ + --group rg-trading \ + --output json +``` + +| Current state | Action | +| --- | --- | +| No `trading-db` exists, or its AKS deployment failed with `ServerNameAlreadyExists` | Update the registration to `1.0.7`, then retry the Challenge 06 deployment. | +| `trading-db` already succeeded with `1.0.6` | Keep `1.0.6` registered unless you deliberately plan a replacement. The next deployment with `1.0.7` creates a different, empty server. Radius can garbage-collect the old server because it is no longer in the recipe result. Back up and migrate any required data first. | + +The recipe and helper scripts do not delete or migrate an existing server. Do not delete a +server reported by `ServerNameAlreadyExists`; it can belong to another workshop. + +To update an eligible environment: + +1. Confirm `RESOURCE_GROUP` is this workshop's own Azure resource group, and that + `rad env show env-azure-prod --output json` uses the same Azure provider scope. +2. Publish all three recipes as `1.0.7` with the Stage 2 commands. Skip a tag that + already exists rather than overwriting it. +3. Rerun the Stage 3 `rad deploy iac/aks-env.bicep` command. Its + `postgresRecipeTemplatePath` must end in `postgres-azure-flex:1.0.7`. +4. Run `rad recipe list --environment env-azure-prod` and verify that + `Radius.Resources/postgreSqlDatabases` references + `${ACR_NAME}.azurecr.io/recipes/postgres-azure-flex:1.0.7`. +5. Retry the unchanged Challenge 06 AKS `rad deploy iac/app.bicep` command. + +The optional automation performs steps 2 and 3 with +`bash resources/configure-recipes.sh aks`. It defaults to `1.0.7` and does not +overwrite an existing version tag. + ## Manual tutorial, Stage 4: Register the complete K3s recipe set Switch both kube context and Radius workspace: @@ -524,7 +590,7 @@ rad deploy iac/local-env.bicep \ --environment env-local-prod \ --parameters environmentName=env-local-prod \ --parameters namespace=env-local-prod \ - --parameters postgresRecipeTemplatePath="${ACR_NAME}.azurecr.io/recipes/postgres-kubernetes:1.0.6" + --parameters postgresRecipeTemplatePath="${ACR_NAME}.azurecr.io/recipes/postgres-kubernetes:1.0.7" ``` **PowerShell 7:** @@ -536,7 +602,7 @@ rad deploy iac/local-env.bicep ` --environment env-local-prod ` --parameters environmentName=env-local-prod ` --parameters namespace=env-local-prod ` - --parameters "postgresRecipeTemplatePath=$env:ACR_NAME.azurecr.io/recipes/postgres-kubernetes:1.0.6" + --parameters "postgresRecipeTemplatePath=$env:ACR_NAME.azurecr.io/recipes/postgres-kubernetes:1.0.7" ``` | Resource type | K3s/local backend | @@ -624,6 +690,7 @@ bash resources/configure-recipes.sh k3s The script creates or reuses ACR, publishes the custom SQL recipe for AKS, deploys the two corrected PostgreSQL recipes plus the custom SQL recipe, deploys the appropriate -environment definition, and verifies the registered recipes. `k3s` also needs -`AZURE_SUBSCRIPTION`, `RESOURCE_GROUP`, and `ACR_NAME` because it republishes the pinned -workshop recipe before registration. +environment definition, and verifies the registered recipes. It uses +`WORKSHOP_RECIPE_VERSION=1.0.7` by default and reuses an existing version tag instead of +overwriting it. `k3s` also needs `AZURE_SUBSCRIPTION`, `RESOURCE_GROUP`, and `ACR_NAME` +because it publishes the pinned workshop recipe before registration when needed. diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md index 1cf101f59..e675962f6 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md @@ -1035,6 +1035,28 @@ Run `rad recipe list --environment ` in the matching workspace. Ret Challenge 05 and deploy `iac/local-env.bicep` or `iac/aks-env.bicep`. Do not replace a portable resource with a platform-specific declaration. +### `ServerNameAlreadyExists` for `trading-db` + +Azure PostgreSQL server names are globally unique. Recipe `1.0.6` derived the name only +from the Radius resource ID, so another workshop with the same group, application, and +resource names could already own it. Do not delete the reported server; it can belong to +another workshop. + +Confirm that this workshop has its own Azure resource group, then inspect the registration: + +```bash +rad recipe list --environment env-azure-prod +``` + +If `Radius.Resources/postgreSqlDatabases` still references `postgres-azure-flex:1.0.6`, +follow [Update an existing AKS environment to recipe `1.0.7`](https://github.com/microsoft/MicroHack/blob/main/03-Azure/01-01-App%20Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md#update-an-existing-aks-environment-to-recipe-107). +Verify that the registration references `postgres-azure-flex:1.0.7`, then rerun the +unchanged AKS `rad deploy iac/app.bicep` command from Stage 3. Retries use the same +`1.0.7` server name. + +If the registration already uses `1.0.7`, check that the environment's Azure provider +scope is not shared with another workshop that uses the same Radius resource names. + ### The deployment reached the wrong platform Check all four dimensions: diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-07/solution-07.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-07/solution-07.md index 64fde3978..cc119a108 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-07/solution-07.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-07/solution-07.md @@ -908,6 +908,19 @@ Use the AKS client secret captured in Stage 3, step 2. The endpoint values match because both portfolios use the same service and realm names; the secret and upstream provider differ. +> [!IMPORTANT] +> This redeploys `iac/app.bicep`, so the Azure PostgreSQL recipe runs again. Keep the +> same recipe registration used for the healthy Challenge 06 deployment. Record the +> database host before deploying, then confirm it is unchanged afterward: +> +> ```bash +> rad resource show Radius.Resources/postgreSqlDatabases trading-db \ +> --workspace ws-azure-prod --group rg-trading --output json | jq -r '.properties.host' +> ``` +> +> A different host means a replacement server, not a rename. Stop and follow the +> recipe-version guidance in Challenge 05 before continuing. + **Bash:** ```bash diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-09/solution-09.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-09/solution-09.md index cb8f5fea6..380ba3f74 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-09/solution-09.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-09/solution-09.md @@ -1130,7 +1130,9 @@ together. The frontend can reach the newly created `ai-agent` service by its stable Radius container name. Challenge 09 does not redeploy `iac/app.bicep`, so Challenge 07's -target-specific OIDC client secret and broker configuration are not overwritten. +target-specific OIDC client secret and broker configuration are not overwritten. The +Challenge 06 `trading-db` and its Azure PostgreSQL server also remain in place; no +AI-recipe change is needed for the PostgreSQL naming correction. ## Optional cleanup From 1df38e20f46989905fa7a8f8e5183e273ab4357f Mon Sep 17 00:00:00 2001 From: Dylan de Jong Date: Thu, 22 Oct 2026 19:15:20 +0200 Subject: [PATCH 2/6] Update Adaptive Apps workshop configuration and database verification Include walkthrough and automation updates, explicit Azure resource groups, ACR input validation, PostgreSQL recipe 1.0.8 with larger default compute, and the shared trading database verification helper. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../challenges/challenge-05.md | 25 +- .../challenges/challenge-06.md | 2 +- .../04-adaptive-apps/iac/app.bicep | 2 +- .../iac/recipes/postgres-azure-flex.bicep | 2 +- .../04-adaptive-apps/labautomation/README.md | 5 +- .../labautomation/console-helpers.ps1 | 12 +- .../labautomation/tests/console.Tests.ps1 | 8 +- .../resources/configure-recipes.sh | 7 +- .../04-adaptive-apps/resources/prepare-aks.sh | 5 +- .../resources/prepare-k3s-azure-vm.sh | 19 +- .../resources/validate-postgres-recipes.sh | 11 + .../resources/verify-trading-db.sh | 137 +++++++ .../tests/test_console_helpers.py | 32 ++ .../walkthrough/challenge-02/solution-02.md | 4 +- .../walkthrough/challenge-03/solution-03.md | 39 +- .../walkthrough/challenge-05/solution-05.md | 101 ++--- .../walkthrough/challenge-06/solution-06.md | 361 +----------------- .../walkthrough/challenge-09/solution-09.md | 6 +- 18 files changed, 329 insertions(+), 449 deletions(-) create mode 100644 03-Azure/01-01-App Innovation/04-adaptive-apps/resources/verify-trading-db.sh diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/challenges/challenge-05.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/challenges/challenge-05.md index 89a8baddb..07a149c40 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/challenges/challenge-05.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/challenges/challenge-05.md @@ -60,11 +60,30 @@ administrator password. While targeting AKS and workspace `ws-azure-prod`: +> [!IMPORTANT] +> `ACR_NAME` must contain only the Azure Container Registry resource name, not a URL +> or the login-server hostname. +> +> ```bash +> # Correct +> export ACR_NAME="acad3zqjinwxy6mh2" +> +> # Incorrect +> export ACR_NAME="acad3zqjinwxy6mh2.azurecr.io" +> ``` +> +> The commands below append `.azurecr.io` automatically. Including it in `ACR_NAME` +> produces a duplicated hostname and causes Radius recipe downloads to fail with +> `RecipeDownloadFailed` / `no such host`. +> +> Hosted participants should source `artifacts/console-env.sh` and retain its supplied +> `ACR_NAME`. + 1. Hosted: use the supplied `ACR_NAME` from `artifacts/console-env.sh` without recreating it. BYOS: create or select a globally unique, lowercase registry. 2. Authenticate to the registry. 3. Publish `iac/recipes/sql-server.bicep` and the workshop-owned Azure and Kubernetes - PostgreSQL recipes as version `1.0.7`. Never overwrite an existing version tag. + PostgreSQL recipes as version `1.0.8`. Never overwrite an existing version tag. 4. Register it as the default recipe for `Radius.Resources/sqlDatabases` in `env-azure-prod`. 5. Verify that the registration refers to the expected OCI artifact. @@ -120,8 +139,8 @@ Prepare a comparison that answers: ## Success criteria -- The custom SQL recipe is published to ACR at version `1.0.7`. -- Both corrected PostgreSQL recipes are published to ACR at version `1.0.7` and +- The custom SQL recipe is published to ACR at version `1.0.8`. +- Both corrected PostgreSQL recipes are published to ACR at version `1.0.8` and registered without mutable `latest` references. - The Azure PostgreSQL server name is deterministic and scoped to both the workshop's Azure resource group and the Radius resource ID. diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/challenges/challenge-06.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/challenges/challenge-06.md index d7b3218ba..945b5fc57 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/challenges/challenge-06.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/challenges/challenge-06.md @@ -152,7 +152,7 @@ Capture a side-by-side comparison that shows: 2. The application needs the extension package generated from the Challenge 04 catalog. 3. A `RecipeNotFoundFailure` is an environment-layer problem. Return to Challenge 05. 4. `ServerNameAlreadyExists` for PostgreSQL is also an environment-layer problem. Use a - workshop-specific Azure resource group, register Azure PostgreSQL recipe `1.0.7`, + workshop-specific Azure resource group, register Azure PostgreSQL recipe `1.0.8`, then retry the unchanged deployment. ### Deployment and validation diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/iac/app.bicep b/03-Azure/01-01-App Innovation/04-adaptive-apps/iac/app.bicep index bec7f97e6..6ecaecfa5 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/iac/app.bicep +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/iac/app.bicep @@ -107,7 +107,7 @@ resource tradingDb 'Radius.Resources/postgreSqlDatabases@2025-08-01-preview' = { properties: { environment: environment application: tradingApp.id - size: 'S' + size: 'L' } } diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/iac/recipes/postgres-azure-flex.bicep b/03-Azure/01-01-App Innovation/04-adaptive-apps/iac/recipes/postgres-azure-flex.bicep index 1c5792dd9..ad048fb0f 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/iac/recipes/postgres-azure-flex.bicep +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/iac/recipes/postgres-azure-flex.bicep @@ -33,7 +33,7 @@ var skuBySize = { tier: 'Burstable' } L: { - name: 'Standard_D2s_v3' + name: 'Standard_D4ds_v5' tier: 'GeneralPurpose' } } diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/labautomation/README.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/labautomation/README.md index 6dc7f34bb..279bcf742 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/labautomation/README.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/labautomation/README.md @@ -89,8 +89,9 @@ override the ordered regions before provisioning. in both regional and DSv5-family quota: 12 baseline plus 4 for AKS surge. Five labs require 80 free vCPUs (60 steady-state). The check budgets the entire fan-out as new capacity, including on reruns. -5. Check PostgreSQL 16 / Burstable `Standard_B1ms` availability for the later - participant recipe; this does not provision a database. +5. Check PostgreSQL 16 / General Purpose `Standard_D4ds_v5` availability for the later + participant application (`size: 'L'`, 4 vCores, 16 GiB RAM); this does not provision + a database or guarantee allocation capacity. 6. Apply the hosted tag initiative and verify inheritance with a temporary RG/NSG. 7. Persist eligible regions and lab count as subscription tags. diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/labautomation/console-helpers.ps1 b/03-Azure/01-01-App Innovation/04-adaptive-apps/labautomation/console-helpers.ps1 index 80606ae83..4ce125f75 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/labautomation/console-helpers.ps1 +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/labautomation/console-helpers.ps1 @@ -43,19 +43,19 @@ function Test-AdaptiveRegion { if ($capability.status -and $capability.status -ne 'Available') { continue } if ($capability.supportedFeatures | Where-Object { $_.name -eq 'OfferRestricted' -and $_.status -eq 'Enabled' }) { continue } # Current CLI uses server editions; older runners return flexible editions per zone. - $burst = @($capability.supportedServerEditions | Where-Object { $_.name -eq 'Burstable' }) + $generalPurpose = @($capability.supportedServerEditions | Where-Object { $_.name -eq 'GeneralPurpose' }) $versions = @($capability.supportedServerVersions | Where-Object { $_.name -eq '16' }) - if ($versions.Count -gt 0 -and ($burst.supportedServerSkus | Where-Object { $_.name -eq 'Standard_B1ms' })) { + if ($versions.Count -gt 0 -and ($generalPurpose.supportedServerSkus | Where-Object { $_.name -eq 'Standard_D4ds_v5' })) { $postgresReady = $true } - $legacyBurst = @($capability.supportedFlexibleServerEditions | Where-Object { $_.name -eq 'Burstable' }) - $legacyVersion = @($legacyBurst.supportedServerVersions | Where-Object { $_.name -eq '16' }) - if ($legacyVersion.supportedVcores | Where-Object { $_.name -eq 'Standard_B1ms' }) { + $legacyGeneralPurpose = @($capability.supportedFlexibleServerEditions | Where-Object { $_.name -eq 'GeneralPurpose' }) + $legacyVersion = @($legacyGeneralPurpose.supportedServerVersions | Where-Object { $_.name -eq '16' }) + if ($legacyVersion.supportedVcores | Where-Object { $_.name -eq 'Standard_D4ds_v5' }) { $postgresReady = $true } } if (-not $postgresReady) { - throw "$Location does not advertise PostgreSQL 16 Burstable Standard_B1ms required by the default recipe." + throw "$Location does not advertise PostgreSQL 16 GeneralPurpose Standard_D4ds_v5 required by the default application." } return $Location } diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/labautomation/tests/console.Tests.ps1 b/03-Azure/01-01-App Innovation/04-adaptive-apps/labautomation/tests/console.Tests.ps1 index 9a75fa14a..df38334a2 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/labautomation/tests/console.Tests.ps1 +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/labautomation/tests/console.Tests.ps1 @@ -25,8 +25,8 @@ BeforeAll { function kubectl { } function rad { } $subscription = '11111111-1111-1111-1111-111111111111' - $postgresCurrent = '[{"supportedFeatures":[{"name":"OfferRestricted","status":"Disabled"}],"supportedServerVersions":[{"name":"16"}],"supportedServerEditions":[{"name":"Burstable","supportedServerSkus":[{"name":"Standard_B1ms"}]}]}]' - $postgresLegacy = '[{"status":"Available","supportedFlexibleServerEditions":[{"name":"Burstable","supportedServerVersions":[{"name":"16","supportedVcores":[{"name":"Standard_B1ms"}]}]}]}]' + $postgresCurrent = '[{"supportedFeatures":[{"name":"OfferRestricted","status":"Disabled"}],"supportedServerVersions":[{"name":"16"}],"supportedServerEditions":[{"name":"GeneralPurpose","supportedServerSkus":[{"name":"Standard_D4ds_v5"}]}]}]' + $postgresLegacy = '[{"status":"Available","supportedFlexibleServerEditions":[{"name":"GeneralPurpose","supportedServerVersions":[{"name":"16","supportedVcores":[{"name":"Standard_D4ds_v5"}]}]}]}]' } Describe 'Regional preflight' { @@ -70,8 +70,8 @@ Describe 'Regional preflight' { $script:postgres = $postgresLegacy Test-AdaptiveRegion -Location westeurope -LabCount 2 | Should -Be 'westeurope' } - It 'rejects a region without PostgreSQL 16 B1ms' { - $script:postgres = $postgresCurrent.Replace('Standard_B1ms', 'Standard_D2s_v3') + It 'rejects a region without PostgreSQL 16 D4ds_v5' { + $script:postgres = $postgresCurrent.Replace('Standard_D4ds_v5', 'Standard_B1ms') { Test-AdaptiveRegion -Location westeurope -LabCount 2 } | Should -Throw '*PostgreSQL 16*' } It 'rejects offer-restricted PostgreSQL' { diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/configure-recipes.sh b/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/configure-recipes.sh index c99fea076..fd7d472a6 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/configure-recipes.sh +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/configure-recipes.sh @@ -27,7 +27,7 @@ AKS_ENVIRONMENT="${AKS_ENVIRONMENT:-env-azure-prod}" K3S_ENVIRONMENT="${K3S_ENVIRONMENT:-env-local-prod}" RADIUS_GROUP="${RADIUS_GROUP:-rg-trading}" RECIPE_REGISTRY="${RECIPE_REGISTRY:-ghcr.io/microsoft/adaptive-apps/recipes}" -WORKSHOP_RECIPE_VERSION="${WORKSHOP_RECIPE_VERSION:-1.0.7}" +WORKSHOP_RECIPE_VERSION="${WORKSHOP_RECIPE_VERSION:-1.0.8}" [[ "$WORKSHOP_RECIPE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "WORKSHOP_RECIPE_VERSION must be an immutable semantic version such as 1.0.0." >&2 exit 1 @@ -120,6 +120,11 @@ publish_workshop_recipes() { : "${RESOURCE_GROUP:?Set RESOURCE_GROUP for workshop recipe publishing.}" : "${ACR_NAME:?Set a globally unique lowercase ACR_NAME.}" + if [[ ! "$ACR_NAME" =~ ^[a-z0-9]{5,50}$ ]]; then + echo "ACR_NAME must be the 5-50 character lowercase alphanumeric registry name, without .azurecr.io or https://." >&2 + exit 1 + fi + source "$(dirname "${BASH_SOURCE[0]}")/azure-session.sh" if ! az acr show \ diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/prepare-aks.sh b/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/prepare-aks.sh index 705a852ab..95cf4acc2 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/prepare-aks.sh +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/prepare-aks.sh @@ -5,6 +5,9 @@ set -euo pipefail # AKS credentials belong in the default kubeconfig, not a previously selected K3s file. unset KUBECONFIG +: "${AZURE_SUBSCRIPTION:?Set AZURE_SUBSCRIPTION to the target subscription ID.}" +: "${RESOURCE_GROUP:?Set RESOURCE_GROUP to the lab Azure resource group.}" + for command_name in az grep kubectl; do command -v "$command_name" >/dev/null 2>&1 || { echo "Required command not found: $command_name" >&2 @@ -12,9 +15,7 @@ for command_name in az grep kubectl; do } done -: "${AZURE_SUBSCRIPTION:?Set AZURE_SUBSCRIPTION to the target subscription ID.}" AZURE_LOCATION="${AZURE_LOCATION:-westeurope}" -RESOURCE_GROUP="${RESOURCE_GROUP:-rg-adaptive-apps}" AKS_CLUSTER="${AKS_CLUSTER:-aks-adaptive-apps}" AKS_NODE_COUNT="${AKS_NODE_COUNT:-2}" AKS_NODE_VM_SIZE="${AKS_NODE_VM_SIZE:-Standard_D4s_v5}" diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/prepare-k3s-azure-vm.sh b/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/prepare-k3s-azure-vm.sh index c036c308a..fbb8e903f 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/prepare-k3s-azure-vm.sh +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/prepare-k3s-azure-vm.sh @@ -17,7 +17,7 @@ readonly K3S_API_PORT="6443" readonly KUBECONFIG_CHUNK_SIZE="1800" AZURE_LOCATION="${AZURE_LOCATION:-westeurope}" -RESOURCE_GROUP="${RESOURCE_GROUP:-rg-adaptive-apps}" +RESOURCE_GROUP="${RESOURCE_GROUP:-}" VNET_NAME="${VNET_NAME:-vnet-adaptive-apps}" VNET_PREFIX="${VNET_PREFIX:-10.42.0.0/16}" K3S_SUBNET_NAME="${K3S_SUBNET_NAME:-snet-k3s}" @@ -69,6 +69,10 @@ Usage: bash resources/prepare-k3s-azure-vm.sh [provision|connect|status|disconne Required for provision/connect: AZURE_SUBSCRIPTION Target Azure subscription ID. + RESOURCE_GROUP Lab Azure resource group. + +For status/disconnect of a running tunnel, retain the same RESOURCE_GROUP so its +process identity can be verified. These modes do not require Azure authentication. The kubeconfig endpoint defaults to https://127.0.0.1:16443. Override the local port with K3S_LOCAL_PORT before provisioning and on every later connect command. @@ -106,6 +110,7 @@ tunnel_pid_matches() { local pid="$1" local command_line + [[ -n "$RESOURCE_GROUP" ]] || return 1 is_pid_running "$pid" || return 1 [[ -r "/proc/${pid}/cmdline" ]] || return 1 command_line="$(tr '\0' ' ' <"/proc/${pid}/cmdline")" @@ -216,6 +221,10 @@ disconnect_tunnel() { } if [[ "$MODE" == "status" ]]; then + if [[ -z "$RESOURCE_GROUP" && -f "$TUNNEL_PID_FILE" ]]; then + echo "Set RESOURCE_GROUP to the lab Azure resource group to verify the recorded tunnel." >&2 + exit 1 + fi if tunnel_status; then exit 0 fi @@ -223,12 +232,19 @@ if [[ "$MODE" == "status" ]]; then fi if [[ "$MODE" == "disconnect" ]]; then + if [[ -z "$RESOURCE_GROUP" && -f "$TUNNEL_PID_FILE" ]]; then + echo "Set RESOURCE_GROUP to the lab Azure resource group to verify the recorded tunnel." >&2 + exit 1 + fi if disconnect_tunnel; then exit 0 fi exit 1 fi +: "${AZURE_SUBSCRIPTION:?Set AZURE_SUBSCRIPTION to the target subscription ID.}" +: "${RESOURCE_GROUP:?Set RESOURCE_GROUP to the lab Azure resource group.}" + for command_name in az base64 cut grep install jq kubectl nohup sed tail timeout tr wc; do command -v "$command_name" >/dev/null 2>&1 || { echo "Required command not found: $command_name" >&2 @@ -236,7 +252,6 @@ for command_name in az base64 cut grep install jq kubectl nohup sed tail timeout } done -: "${AZURE_SUBSCRIPTION:?Set AZURE_SUBSCRIPTION to the target subscription ID.}" if ! is_integer "$K3S_LOCAL_PORT"; then echo "K3S_LOCAL_PORT must be an unprivileged TCP port from 1024 through 65535." >&2 exit 1 diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/validate-postgres-recipes.sh b/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/validate-postgres-recipes.sh index 21513a05f..3f5cba32a 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/validate-postgres-recipes.sh +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/validate-postgres-recipes.sh @@ -23,6 +23,17 @@ for recipe in "$AZURE_RECIPE" "$KUBERNETES_RECIPE"; do } done +grep -A8 -F "resource tradingDb " "$APP_TEMPLATE" | grep -Fq "size: 'L'" || { + echo "The default application must request PostgreSQL size L." >&2 + exit 1 +} +large_sku="$(sed -n '/^ L: {/,/^ }/p' "$AZURE_RECIPE")" +grep -Fq "name: 'Standard_D4ds_v5'" <<<"$large_sku" && + grep -Fq "tier: 'GeneralPurpose'" <<<"$large_sku" || { + echo "Azure PostgreSQL size L must use GeneralPurpose Standard_D4ds_v5." >&2 + exit 1 +} + for table in accounts orders trades positions; do grep -Eq "CREATE TABLE IF NOT EXISTS ${table}[[:space:]]*\\(" "$SCHEMA_FILE" || { echo "Shared schema is missing table: $table" >&2 diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/verify-trading-db.sh b/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/verify-trading-db.sh new file mode 100644 index 000000000..46ef20629 --- /dev/null +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/resources/verify-trading-db.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# Invoke with Bash from the Adaptive Apps MicroHack root. +set -euo pipefail + +: "${APP_NAMESPACE:?Set APP_NAMESPACE to the deployed application namespace.}" +for command_name in kubectl jq; do + command -v "$command_name" >/dev/null 2>&1 || { + echo "Required command not found: $command_name" >&2 + exit 1 + } +done + +pod_name="" +cleanup() { + if [[ -n "$pod_name" ]]; then + kubectl delete pod "$pod_name" --namespace "$APP_NAMESPACE" \ + --ignore-not-found --wait=false || { + echo "Could not delete verification pod ${pod_name}; remove it manually." >&2 + return 1 + } + fi +} +trap cleanup EXIT + +echo "Kubernetes context: $(kubectl config current-context)" +echo "Application namespace: ${APP_NAMESPACE}" + +schema_job="$(kubectl get jobs --namespace "$APP_NAMESPACE" \ + --selector app=postgres-schema-initializer,resource=trading-db --output json | + jq -r '.items | sort_by(.metadata.creationTimestamp) | last | .metadata.name // empty')" +[[ -n "$schema_job" ]] || { + echo "No trading-db schema initializer Job found in ${APP_NAMESPACE}." >&2 + exit 1 +} +echo "Using latest schema initializer: ${schema_job}" +if ! kubectl wait --namespace "$APP_NAMESPACE" --for=condition=complete \ + "job/${schema_job}" --timeout=15m; then + kubectl logs --namespace "$APP_NAMESPACE" "job/${schema_job}" || { + echo "Could not retrieve schema initializer logs." >&2 + } + exit 1 +fi + +db_env="$(kubectl get job "$schema_job" --namespace "$APP_NAMESPACE" --output json | + jq '.spec.template.spec.containers[0].env | + map(select(.name == "PGHOST" or .name == "PGDATABASE" or .name == "PGUSER" + or .name == "PGPASSWORD" or .name == "PGSSLMODE")) | + if any(.[]; .name == "PGSSLMODE") then . + else . + [{name: "PGSSLMODE", value: "prefer"}] end')" +jq -e ' + . as $env | + all(["PGHOST", "PGDATABASE", "PGUSER", "PGSSLMODE"][]; + . as $name | any($env[]; .name == $name and + (.value | type == "string" and length > 0))) and + any(.[]; .name == "PGPASSWORD" and + (.valueFrom.secretKeyRef.name | type == "string" and length > 0) and + (.valueFrom.secretKeyRef.key | type == "string" and length > 0)) +' <<<"$db_env" >/dev/null || { + echo "Schema Job is missing database connection settings or its password Secret reference." >&2 + exit 1 +} +jq -r ' + .[] | select(.name == "PGHOST" or .name == "PGDATABASE" or .name == "PGSSLMODE") | + "\(.name)=\(.value)" +' <<<"$db_env" + +kubectl wait --namespace "$APP_NAMESPACE" --for=condition=Available \ + deployment/backend --timeout=5m + +sql_script="$(cat <<'SQL_SCRIPT' +psql --set=ON_ERROR_STOP=1 <<'SQL' +SELECT table_name +FROM information_schema.tables +WHERE table_schema = 'public' + AND table_name IN ('accounts', 'orders', 'trades', 'positions') +ORDER BY table_name; + +SELECT 'demo_seed_count=' || count(*) +FROM accounts +WHERE name = 'Demo Account'; + +SELECT id, account_id, symbol, side, quantity, price, status, created_at +FROM orders +ORDER BY id DESC +LIMIT 20; +SQL +SQL_SCRIPT +)" +backend_script="$(cat <<'BACKEND_SCRIPT' +accounts="$(curl --silent --show-error --fail --connect-timeout 10 --max-time 60 \ + http://backend:8080/api/accounts)" +printf '%s\n' "$accounts" | grep -F '"Demo Account"' +BACKEND_SCRIPT +)" + +pod_name="$(jq -n --arg namespace "$APP_NAMESPACE" --argjson env "$db_env" \ + --arg sql "$sql_script" --arg backend "$backend_script" '{ + apiVersion: "v1", + kind: "Pod", + metadata: {generateName: "trading-db-verify-", namespace: $namespace}, + spec: { + restartPolicy: "Never", + activeDeadlineSeconds: 300, + containers: [ + {name: "psql", image: "postgres:16-alpine", + command: ["sh", "-ceu", $sql], env: $env}, + {name: "backend-check", image: "curlimages/curl:8.12.1", + command: ["sh", "-ceu", $backend]} + ] + } + }' | kubectl create -f - --output jsonpath='{.metadata.name}')" + +echo "Verification pod: ${pod_name}" +for ((attempt = 0; attempt < 150; attempt++)); do + phase="$(kubectl get pod "$pod_name" --namespace "$APP_NAMESPACE" \ + --output jsonpath='{.status.phase}')" + case "$phase" in + Succeeded | Failed) break ;; + esac + sleep 2 +done + +if [[ "$phase" != "Succeeded" ]]; then + echo "Verification did not succeed (pod phase: ${phase})." >&2 + kubectl describe pod "$pod_name" --namespace "$APP_NAMESPACE" || { + echo "Could not describe verification pod." >&2 + } +fi +for container in psql backend-check; do + echo "=== ${container} ===" + kubectl logs "$pod_name" --namespace "$APP_NAMESPACE" --container "$container" || { + echo "Could not retrieve ${container} logs." >&2 + exit 1 + } +done +[[ "$phase" == "Succeeded" ]] || exit 1 +echo "Database queries and backend Demo Account check passed." diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/tests/test_console_helpers.py b/03-Azure/01-01-App Innovation/04-adaptive-apps/tests/test_console_helpers.py index e7c1c77fe..6c9c8c34f 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/tests/test_console_helpers.py +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/tests/test_console_helpers.py @@ -173,6 +173,38 @@ def run_script(self, name, *args, success=True, **overrides): def commands(self): return [json.loads(line) for line in self.log.read_text().splitlines()] if self.log.exists() else [] + def test_platform_preparation_requires_explicit_azure_resource_group(self): + for script, mode in (("prepare-aks.sh", ()), + ("prepare-k3s-azure-vm.sh", ("provision",)), + ("prepare-k3s-azure-vm.sh", ("connect",))): + with self.subTest(script=script, mode=mode): + result = self.run_script(script, *mode, success=False, RESOURCE_GROUP="") + self.assertIn("Set RESOURCE_GROUP", result.stderr) + self.assertEqual(self.commands(), []) + + def test_k3s_local_modes_without_recorded_tunnel_need_no_azure_configuration(self): + for mode in ("--help", "status", "disconnect"): + with self.subTest(mode=mode): + result = self.run_script( + "prepare-k3s-azure-vm.sh", mode, success=mode != "status", + AZURE_SUBSCRIPTION="", RESOURCE_GROUP="") + self.assertNotIn("Set RESOURCE_GROUP", result.stderr) + self.assertEqual(self.commands(), []) + + def test_k3s_recorded_tunnel_requires_group_for_identity_verification(self): + state = self.root / "recorded-tunnel" + state.mkdir() + (state / "tunnel.pid").write_text("99999999") + for mode in ("status", "disconnect"): + with self.subTest(mode=mode): + result = self.run_script( + "prepare-k3s-azure-vm.sh", mode, success=False, + AZURE_SUBSCRIPTION="", RESOURCE_GROUP="", + K3S_TUNNEL_STATE_DIR=str(state)) + self.assertIn("Set RESOURCE_GROUP", result.stderr) + self.assertTrue((state / "tunnel.pid").exists()) + self.assertEqual(self.commands(), []) + def assert_no_provisioning(self): for command in self.commands(): if command[0] == "az": diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-02/solution-02.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-02/solution-02.md index fadbc3100..5daac50c0 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-02/solution-02.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-02/solution-02.md @@ -65,7 +65,7 @@ pwd export AZURE_SUBSCRIPTION="" export AZURE_LOCATION="westeurope" -export RESOURCE_GROUP="rg-adaptive-apps" +export RESOURCE_GROUP="${RESOURCE_GROUP:-}" export AKS_CLUSTER="aks-adaptive-apps" bash resources/prepare-aks.sh @@ -101,7 +101,7 @@ Bastion-restricted Linux troubleshooting route. ```bash export AZURE_SUBSCRIPTION="" export AZURE_LOCATION="westeurope" -export RESOURCE_GROUP="rg-adaptive-apps" +export RESOURCE_GROUP="${RESOURCE_GROUP:-}" export K3S_VM_NAME="vm-adaptive-apps-k3s" bash resources/prepare-k3s-azure-vm.sh diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-03/solution-03.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-03/solution-03.md index 9ea19ef85..0f997003f 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-03/solution-03.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-03/solution-03.md @@ -17,17 +17,6 @@ for Azure workload identity. - Radius startup can take 5-10 minutes. Inspect pod state before rerunning commands. - Always compare `kubectl config current-context` with the active Radius workspace. -Before any K3s work after a devcontainer restart, restore the private API tunnel: - -```bash -export AZURE_SUBSCRIPTION="${AZURE_SUBSCRIPTION:-}" -bash resources/prepare-k3s-azure-vm.sh connect -export KUBECONFIG="$HOME/.kube/adaptive-apps-k3s.yaml" -``` - -The tunnel exposes only the Kubernetes API on localhost. Radius dashboard access still -uses `kubectl port-forward` through that API connection. - ## Control-plane deployment options ### Federated control planes @@ -130,6 +119,20 @@ Workstation rad configuration ### 1. Select and verify AKS +If the `aks-adaptive-apps` context is missing, connect to your lab's AKS cluster: + +```bash +export AZURE_SUBSCRIPTION="${AZURE_SUBSCRIPTION:-}" +export RESOURCE_GROUP="${RESOURCE_GROUP:-}" +unset KUBECONFIG +az aks get-credentials \ + --resource-group "$RESOURCE_GROUP" \ + --name aks-adaptive-apps \ + --context aks-adaptive-apps +``` + +Then select and verify it: + ```bash unset KUBECONFIG kubectl config use-context aks-adaptive-apps @@ -142,11 +145,12 @@ kubectl get namespace radius-system --ignore-not-found Hosted participants first source `artifacts/console-env.sh`; the defaults below preserve those supplied values. BYOS participants set their own subscription and -resource group. Do not replace a Console resource group with `rg-adaptive-apps`. +resource group. Replace `` with your lab's Azure resource-group +name; keep the value supplied by Console if already set. ```bash export AZURE_SUBSCRIPTION="${AZURE_SUBSCRIPTION:-}" -export RESOURCE_GROUP="${RESOURCE_GROUP:-rg-adaptive-apps}" +export RESOURCE_GROUP="${RESOURCE_GROUP:-}" export AKS_CLUSTER="${AKS_CLUSTER:-aks-adaptive-apps}" export RADIUS_APP_NAME="${AKS_CLUSTER}-radius-app" @@ -163,7 +167,7 @@ Choose **one** identity option. Both use AKS OIDC federation with no client secr See Microsoft's [AKS workload identity guide](https://learn.microsoft.com/azure/aks/workload-identity-deploy-cluster) for the managed identity and federated credential commands. -#### Option A: user-assigned managed identity (required for hosted Console) +#### Option A: user-assigned managed identity (required for MicroHack Console) Create the identity inside the supplied lab resource group. These are Azure Resource Manager operations, not tenant app/SP creation. The explicit principal object ID @@ -206,7 +210,7 @@ at **Grant the identity access**. #### Option B: Entra application (BYOS with tenant permissions only) -Create an Entra application and service principal. Do not run this option in a +BYOS: Create an Entra application and service principal. Do not run this option in a hosted RG-only lab: ```bash @@ -226,7 +230,7 @@ export IDENTITY_PRINCIPAL_ID="$(az ad sp show \ --id "$APPLICATION_CLIENT_ID" --query id --output tsv)" ``` -For each Radius service account, create a federated credential. The example below is +BYOS: For each Radius service account, create a federated credential. The example below is for `applications-rp`; repeat it for `bicep-de`, `ucp`, and `dynamic-rp`, changing both `name` and `subject`: @@ -307,6 +311,7 @@ rad credential register azure wi \ ```bash export AZURE_SUBSCRIPTION="${AZURE_SUBSCRIPTION:-}" +export RESOURCE_GROUP="${RESOURCE_GROUP:-}" bash resources/prepare-k3s-azure-vm.sh connect export KUBECONFIG="$HOME/.kube/adaptive-apps-k3s.yaml" kubectl config use-context k3s-azure-vm @@ -412,7 +417,7 @@ unset KUBECONFIG kubectl config use-context aks-adaptive-apps export AZURE_SUBSCRIPTION="${AZURE_SUBSCRIPTION:-}" -export RESOURCE_GROUP="${RESOURCE_GROUP:-rg-adaptive-apps}" +export RESOURCE_GROUP="${RESOURCE_GROUP:-}" export AKS_CLUSTER="aks-adaptive-apps" # Run from the Adaptive Apps MicroHack root. diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md index 02e769825..e9e30ce00 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md @@ -157,7 +157,7 @@ kubectl get jobs -n Azure Database for PostgreSQL Flexible Server names are globally unique. Separate workshops commonly use the same Radius resource ID, for example the same group, application, and `trading-db` resource names on different clusters. Recipe version -`1.0.7` therefore scopes the deterministic server name to the target Azure resource group: +`1.0.8` retains the deterministic server name scoped to the target Azure resource group: ```bicep var serverName = 'pg-${uniqueString(resourceGroup().id, context.resource.id)}' @@ -260,6 +260,25 @@ rad env switch env-azure-prod rad group switch rg-trading ``` +> [!IMPORTANT] +> `ACR_NAME` must contain only the Azure Container Registry resource name, not a URL +> or the login-server hostname. +> +> ```bash +> # Correct +> export ACR_NAME="acad3zqjinwxy6mh2" +> +> # Incorrect +> export ACR_NAME="acad3zqjinwxy6mh2.azurecr.io" +> ``` +> +> The commands below append `.azurecr.io` automatically. Including it in `ACR_NAME` +> produces a duplicated hostname and causes Radius recipe downloads to fail with +> `RecipeDownloadFailed` / `no such host`. +> +> Hosted participants should source `artifacts/console-env.sh` and retain its supplied +> `ACR_NAME`. + **Hosted Console:** reuse the supplied registry; do not run the creation block. Source and verify your supplied values: @@ -273,7 +292,7 @@ az acr show --resource-group "$RESOURCE_GROUP" --name "$ACR_NAME" \ ```bash export AZURE_SUBSCRIPTION="${AZURE_SUBSCRIPTION:-}" -export RESOURCE_GROUP="${RESOURCE_GROUP:-rg-adaptive-apps}" +export RESOURCE_GROUP="${RESOURCE_GROUP:-}" export ACR_NAME="${ACR_NAME:-}" az account set --subscription "$AZURE_SUBSCRIPTION" @@ -329,22 +348,25 @@ EOF Publish the teaching recipe and both workshop-owned PostgreSQL recipes. The PostgreSQL recipes compile the same `iac/recipes/trading-schema.sql` into each implementation and -use immutable workshop tags rather than the external `latest` recipes. Version `1.0.7` -contains the Azure PostgreSQL naming correction. Do not republish changed source over +use immutable workshop tags rather than the external `latest` recipes. Version `1.0.8` +retains the Azure PostgreSQL naming correction and maps size `L` to General Purpose +`Standard_D4ds_v5` (4 vCores, 16 GiB RAM), as requested by the default application. +PostgreSQL remains version 16 with 32 GiB storage. In K3s, the same portable size `L` +selects a 2 GiB memory limit rather than Azure compute. Do not republish changed source over `1.0.6` or another existing tag; publish a new version instead. ```bash rad bicep publish \ --file iac/recipes/sql-server.bicep \ - --target "br:${ACR_NAME}.azurecr.io/recipes/sql-server:1.0.7" + --target "br:${ACR_NAME}.azurecr.io/recipes/sql-server:1.0.8" rad bicep publish \ --file iac/recipes/postgres-azure-flex.bicep \ - --target "br:${ACR_NAME}.azurecr.io/recipes/postgres-azure-flex:1.0.7" + --target "br:${ACR_NAME}.azurecr.io/recipes/postgres-azure-flex:1.0.8" rad bicep publish \ --file iac/recipes/postgres-kubernetes.bicep \ - --target "br:${ACR_NAME}.azurecr.io/recipes/postgres-kubernetes:1.0.7" + --target "br:${ACR_NAME}.azurecr.io/recipes/postgres-kubernetes:1.0.8" ``` **PowerShell 7:** @@ -352,15 +374,15 @@ rad bicep publish \ ```powershell rad bicep publish ` --file iac/recipes/sql-server.bicep ` - --target "br:$env:ACR_NAME.azurecr.io/recipes/sql-server:1.0.7" + --target "br:$env:ACR_NAME.azurecr.io/recipes/sql-server:1.0.8" rad bicep publish ` --file iac/recipes/postgres-azure-flex.bicep ` - --target "br:$env:ACR_NAME.azurecr.io/recipes/postgres-azure-flex:1.0.7" + --target "br:$env:ACR_NAME.azurecr.io/recipes/postgres-azure-flex:1.0.8" rad bicep publish ` --file iac/recipes/postgres-kubernetes.bicep ` - --target "br:$env:ACR_NAME.azurecr.io/recipes/postgres-kubernetes:1.0.7" + --target "br:$env:ACR_NAME.azurecr.io/recipes/postgres-kubernetes:1.0.8" ``` Register the Azure SQL teaching recipe: @@ -370,7 +392,7 @@ rad recipe register default \ --environment env-azure-prod \ --resource-type Radius.Resources/sqlDatabases \ --template-kind bicep \ - --template-path "${ACR_NAME}.azurecr.io/recipes/sql-server:1.0.7" + --template-path "${ACR_NAME}.azurecr.io/recipes/sql-server:1.0.8" ``` Verify: @@ -449,9 +471,9 @@ rad deploy iac/aks-env.bicep \ --parameters azureSubscriptionId="$AZURE_SUBSCRIPTION" \ --parameters azureResourceGroup="$RESOURCE_GROUP" \ --parameters istioRevision="$ISTIO_REVISION" \ - --parameters postgresRecipeTemplatePath="${ACR_NAME}.azurecr.io/recipes/postgres-azure-flex:1.0.7" \ + --parameters postgresRecipeTemplatePath="${ACR_NAME}.azurecr.io/recipes/postgres-azure-flex:1.0.8" \ --parameters aksEgressIps="$AKS_EGRESS_IPS" \ - --parameters sqlDatabasesRecipeTemplatePath="${ACR_NAME}.azurecr.io/recipes/sql-server:1.0.7" + --parameters sqlDatabasesRecipeTemplatePath="${ACR_NAME}.azurecr.io/recipes/sql-server:1.0.8" ``` **PowerShell 7:** @@ -502,9 +524,9 @@ rad deploy iac/aks-env.bicep ` --parameters azureSubscriptionId=$env:AZURE_SUBSCRIPTION ` --parameters azureResourceGroup=$env:RESOURCE_GROUP ` --parameters istioRevision=$IstioRevision ` - --parameters "postgresRecipeTemplatePath=$env:ACR_NAME.azurecr.io/recipes/postgres-azure-flex:1.0.7" ` + --parameters "postgresRecipeTemplatePath=$env:ACR_NAME.azurecr.io/recipes/postgres-azure-flex:1.0.8" ` --parameters "aksEgressIps=$($EgressIps -join ',')" ` - --parameters "sqlDatabasesRecipeTemplatePath=$env:ACR_NAME.azurecr.io/recipes/sql-server:1.0.7" + --parameters "sqlDatabasesRecipeTemplatePath=$env:ACR_NAME.azurecr.io/recipes/sql-server:1.0.8" ``` | Resource type | AKS/Azure backend | @@ -526,47 +548,6 @@ rad deploy iac/aks-env.bicep ` > [Why AKS uses Mosquitto for `mqttBrokers`](https://github.com/microsoft/MicroHack/blob/main/03-Azure/01-01-App%20Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md#why-aks-uses-mosquitto-for-mqttbrokers), > including the `mqttRecipeTemplatePath` override that restores the Event Grid recipe. -### Update an existing AKS environment to recipe `1.0.7` - -Changing the naming algorithm changes the Azure resource name. It is not an in-place -rename. Decide from the current `trading-db` state before changing the registration: - -```bash -rad recipe list \ - --workspace ws-azure-prod \ - --group rg-trading \ - --environment env-azure-prod -rad resource show Radius.Resources/postgreSqlDatabases trading-db \ - --workspace ws-azure-prod \ - --group rg-trading \ - --output json -``` - -| Current state | Action | -| --- | --- | -| No `trading-db` exists, or its AKS deployment failed with `ServerNameAlreadyExists` | Update the registration to `1.0.7`, then retry the Challenge 06 deployment. | -| `trading-db` already succeeded with `1.0.6` | Keep `1.0.6` registered unless you deliberately plan a replacement. The next deployment with `1.0.7` creates a different, empty server. Radius can garbage-collect the old server because it is no longer in the recipe result. Back up and migrate any required data first. | - -The recipe and helper scripts do not delete or migrate an existing server. Do not delete a -server reported by `ServerNameAlreadyExists`; it can belong to another workshop. - -To update an eligible environment: - -1. Confirm `RESOURCE_GROUP` is this workshop's own Azure resource group, and that - `rad env show env-azure-prod --output json` uses the same Azure provider scope. -2. Publish all three recipes as `1.0.7` with the Stage 2 commands. Skip a tag that - already exists rather than overwriting it. -3. Rerun the Stage 3 `rad deploy iac/aks-env.bicep` command. Its - `postgresRecipeTemplatePath` must end in `postgres-azure-flex:1.0.7`. -4. Run `rad recipe list --environment env-azure-prod` and verify that - `Radius.Resources/postgreSqlDatabases` references - `${ACR_NAME}.azurecr.io/recipes/postgres-azure-flex:1.0.7`. -5. Retry the unchanged Challenge 06 AKS `rad deploy iac/app.bicep` command. - -The optional automation performs steps 2 and 3 with -`bash resources/configure-recipes.sh aks`. It defaults to `1.0.7` and does not -overwrite an existing version tag. - ## Manual tutorial, Stage 4: Register the complete K3s recipe set Switch both kube context and Radius workspace: @@ -590,7 +571,7 @@ rad deploy iac/local-env.bicep \ --environment env-local-prod \ --parameters environmentName=env-local-prod \ --parameters namespace=env-local-prod \ - --parameters postgresRecipeTemplatePath="${ACR_NAME}.azurecr.io/recipes/postgres-kubernetes:1.0.7" + --parameters postgresRecipeTemplatePath="${ACR_NAME}.azurecr.io/recipes/postgres-kubernetes:1.0.8" ``` **PowerShell 7:** @@ -602,7 +583,7 @@ rad deploy iac/local-env.bicep ` --environment env-local-prod ` --parameters environmentName=env-local-prod ` --parameters namespace=env-local-prod ` - --parameters "postgresRecipeTemplatePath=$env:ACR_NAME.azurecr.io/recipes/postgres-kubernetes:1.0.7" + --parameters "postgresRecipeTemplatePath=$env:ACR_NAME.azurecr.io/recipes/postgres-kubernetes:1.0.8" ``` | Resource type | K3s/local backend | @@ -677,7 +658,7 @@ The manual tutorial is the intended learning path. To automate the same configur ```bash export AZURE_SUBSCRIPTION="${AZURE_SUBSCRIPTION:-}" -export RESOURCE_GROUP="${RESOURCE_GROUP:-rg-adaptive-apps}" +export RESOURCE_GROUP="${RESOURCE_GROUP:-}" export ACR_NAME="${ACR_NAME:-}" # Configure both defaults: @@ -691,6 +672,6 @@ bash resources/configure-recipes.sh k3s The script creates or reuses ACR, publishes the custom SQL recipe for AKS, deploys the two corrected PostgreSQL recipes plus the custom SQL recipe, deploys the appropriate environment definition, and verifies the registered recipes. It uses -`WORKSHOP_RECIPE_VERSION=1.0.7` by default and reuses an existing version tag instead of +`WORKSHOP_RECIPE_VERSION=1.0.8` by default and reuses an existing version tag instead of overwriting it. `k3s` also needs `AZURE_SUBSCRIPTION`, `RESOURCE_GROUP`, and `ACR_NAME` because it publishes the pinned workshop recipe before registration when needed. diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md index e675962f6..f73c81728 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md @@ -471,7 +471,7 @@ Azure Database for PostgreSQL can take several minutes to provision. ```bash export AZURE_SUBSCRIPTION="${AZURE_SUBSCRIPTION:-}" -export RESOURCE_GROUP="${RESOURCE_GROUP:-rg-adaptive-apps}" +export RESOURCE_GROUP="${RESOURCE_GROUP:-}" az account set --subscription "$AZURE_SUBSCRIPTION" rad app graph --application adaptive-apps @@ -493,7 +493,9 @@ az resource list \ ```powershell $env:AZURE_SUBSCRIPTION = "" -$env:RESOURCE_GROUP = "rg-adaptive-apps" +if (-not $env:RESOURCE_GROUP) { + $env:RESOURCE_GROUP = "" +} az account set --subscription $env:AZURE_SUBSCRIPTION rad app graph --application adaptive-apps @@ -523,7 +525,7 @@ active AKS control plane: ```bash rad resource expose Applications.Core/containers frontend \ --application adaptive-apps \ - --port 3000 \ + --port 3002 \ --remote-port 3000 ``` @@ -565,335 +567,12 @@ the reason the Event Grid implementation is not selected. ### 6. Prove database initialization and backend readiness -`app.bicep` never changes, but the database it resolves to does, so the evidence differs -per platform. On K3s the database is a pod inside the cluster. On AKS it is an Azure -Database for PostgreSQL Flexible Server outside the cluster, so the check runs against -Azure and doubles as proof that the orders you placed in step 5 really left Kubernetes. - -Both checks depend on the recipe-owned schema Job. Wait for it: a Kubernetes-extension -deployment records API acceptance, not Job completion, so the deployment can report -success before the tables exist. The backend readiness probe calls `/api/accounts`, so -Kubernetes does not mark the backend Ready until the schema is queryable. - -#### K3s: check the in-cluster database - -The verification pod reuses the recipe-created Secret through `secretKeyRef`; no command, -manifest, or output contains the database password. It prints only table names and the -number of `Demo Account` rows. - -**Bash:** - -```bash -SCHEMA_JOB="$(kubectl get jobs \ - --namespace "$APP_NAMESPACE" \ - --selector app=postgres-schema-initializer,resource=trading-db \ - --output jsonpath='{.items[0].metadata.name}')" -test -n "$SCHEMA_JOB" -kubectl wait \ - --namespace "$APP_NAMESPACE" \ - --for=condition=complete \ - "job/$SCHEMA_JOB" \ - --timeout=15m - -DB_HOST="$(kubectl get job "$SCHEMA_JOB" --namespace "$APP_NAMESPACE" \ - --output jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="PGHOST")].value}')" -DB_NAME="$(kubectl get job "$SCHEMA_JOB" --namespace "$APP_NAMESPACE" \ - --output jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="PGDATABASE")].value}')" -DB_USER="$(kubectl get job "$SCHEMA_JOB" --namespace "$APP_NAMESPACE" \ - --output jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="PGUSER")].value}')" -DB_SECRET="$(kubectl get job "$SCHEMA_JOB" --namespace "$APP_NAMESPACE" \ - --output jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="PGPASSWORD")].valueFrom.secretKeyRef.name}')" -DB_SSLMODE="$(kubectl get job "$SCHEMA_JOB" --namespace "$APP_NAMESPACE" \ - --output jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="PGSSLMODE")].value}')" -DB_SSLMODE="${DB_SSLMODE:-prefer}" - -cat < [!NOTE] -> Flexible Server has no query editor blade in the Azure portal. The portal route is -> Cloud Shell, which runs the same Azure CLI commands shown below. - -Ask Radius where the database lives. The recipe returns the connection metadata but -deliberately never returns the password, so that still comes from the recipe-created -Kubernetes Secret. - -**Bash:** - -```bash -az extension add --name rdbms-connect - -DB_JSON="$(rad resource show Radius.Resources/postgreSqlDatabases trading-db \ - --output json)" -PG_HOST="$(jq -r '.properties.host' <<<"$DB_JSON")" -PG_DATABASE="$(jq -r '.properties.database' <<<"$DB_JSON")" -PG_USER="$(jq -r '.properties.username' <<<"$DB_JSON")" -PG_SERVER="${PG_HOST%%.*}" -PG_GROUP="$(az postgres flexible-server list \ - --query "[?name=='$PG_SERVER'].resourceGroup | [0]" \ - --output tsv)" -PG_PASSWORD="$(kubectl get secret trading-db-credentials \ - --namespace "$APP_NAMESPACE" \ - --output jsonpath='{.data.password}' | base64 --decode)" - -echo "server=$PG_SERVER group=$PG_GROUP database=$PG_DATABASE user=$PG_USER" -``` - -The recipe allowlisted only the AKS egress IPs, so your own client — laptop, dev -container, or Cloud Shell — cannot reach the server yet. Open a rule for it, then close -it again once the queries are done: +For one combined Bash check on either platform, so run for both AKS and K3S, select the intended Kubernetes context (if lost set `APP_NAMESPACE` to the application's namespace), then run in both AKS and K3S context: ```bash -CLIENT_IP="$(curl -sf https://api.ipify.org)" -az postgres flexible-server firewall-rule create \ - --resource-group "$PG_GROUP" \ - --server-name "$PG_SERVER" \ - --name workshop-client \ - --start-ip-address "$CLIENT_IP" \ - --end-ip-address "$CLIENT_IP" \ - --output none -``` - -Confirm the schema Job created the tables, then read back your orders: - -> [!IMPORTANT] -> Keep each `--querytext` value on a single line. `az postgres flexible-server execute` -> splits the query on newlines and runs only the first fragment, which fails with a -> confusing `column "..." does not exist` instead of an obvious syntax error. The `id` -> alias and the `::text` cast are also deliberate: the CLI's table renderer drops a bare -> `id` column and native timestamp columns. Use `--output json` to see raw values. - -```bash -az postgres flexible-server execute \ - --name "$PG_SERVER" \ - --admin-user "$PG_USER" \ - --admin-password "$PG_PASSWORD" \ - --database-name "$PG_DATABASE" \ - --querytext "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name;" \ - --output table - -az postgres flexible-server execute \ - --name "$PG_SERVER" \ - --admin-user "$PG_USER" \ - --admin-password "$PG_PASSWORD" \ - --database-name "$PG_DATABASE" \ - --querytext "SELECT id AS order_id, symbol, side, order_type, quantity, price, status, created_at::text AS placed_at FROM orders ORDER BY id;" \ - --output table - -az postgres flexible-server firewall-rule delete \ - --resource-group "$PG_GROUP" \ - --server-name "$PG_SERVER" \ - --name workshop-client \ - --yes -``` - -**PowerShell 7:** - -```powershell -az extension add --name rdbms-connect - -$Database = rad resource show Radius.Resources/postgreSqlDatabases trading-db ` - --output json | - ConvertFrom-Json -$PgHost = $Database.properties.host -$PgDatabase = $Database.properties.database -$PgUser = $Database.properties.username -$PgServer = $PgHost.Split(".")[0] -$PgGroup = az postgres flexible-server list ` - --query "[?name=='$PgServer'].resourceGroup | [0]" ` - --output tsv -$PgPassword = [Text.Encoding]::UTF8.GetString( - [Convert]::FromBase64String((kubectl get secret trading-db-credentials ` - --namespace $AppNamespace ` - --output jsonpath='{.data.password}'))) - -"server=$PgServer group=$PgGroup database=$PgDatabase user=$PgUser" - -$ClientIp = "$(Invoke-RestMethod -Uri 'https://api.ipify.org')".Trim() -az postgres flexible-server firewall-rule create ` - --resource-group $PgGroup ` - --server-name $PgServer ` - --name workshop-client ` - --start-ip-address $ClientIp ` - --end-ip-address $ClientIp ` - --output none - -az postgres flexible-server execute ` - --name $PgServer ` - --admin-user $PgUser ` - --admin-password $PgPassword ` - --database-name $PgDatabase ` - --querytext "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name;" ` - --output table - -az postgres flexible-server execute ` - --name $PgServer ` - --admin-user $PgUser ` - --admin-password $PgPassword ` - --database-name $PgDatabase ` - --querytext "SELECT id AS order_id, symbol, side, order_type, quantity, price, status, created_at::text AS placed_at FROM orders ORDER BY id;" ` - --output table - -az postgres flexible-server firewall-rule delete ` - --resource-group $PgGroup ` - --server-name $PgServer ` - --name workshop-client ` - --yes +bash resources/verify-trading-db.sh ``` +You will see the entered trading records per environment, they should differe and this step proof that the chain is working. The first query lists `accounts`, `orders`, `positions`, and `trades`. The second returns one row per order you placed in step 5: @@ -905,18 +584,9 @@ Order_id Order_type Placed_at Price Quantity Si 2 MARKET 2026-09-02 12:36:09.462464+00 384.04 10 BUY processed TSLA ``` -An uncovered SELL shows up here too, with `status` `rejected`. Those rows are in Azure, -not in the cluster: the pods you were talking to contain no database at all. Nothing in -`app.bicep` changed to make that happen; only the recipe registered for -`Radius.Resources/postgreSqlDatabases` differs between the two environments. - -`PGSSLMODE=require` on the schema Job proves the initializer uses TLS. The recipe also -enables `require_secure_transport` and a TLS 1.2 floor on the server, so the backend's -Npgsql connection negotiates TLS and the server rejects plaintext sessions. > [!TIP] -> To avoid touching the firewall at all, run the query from inside the cluster, whose -> egress IP the recipe already allowlisted. This also avoids the CLI's table-rendering +> To avoid touching the firewall at all, add the client ip in the Azure portal OR run > the query from inside the cluster, whose egress IP the recipe already allowlisted. > This also avoids the CLI's table-rendering > quirks, because you get raw `psql` output, and the pod deletes itself on exit: > > ```bash @@ -1048,13 +718,14 @@ Confirm that this workshop has its own Azure resource group, then inspect the re rad recipe list --environment env-azure-prod ``` -If `Radius.Resources/postgreSqlDatabases` still references `postgres-azure-flex:1.0.6`, -follow [Update an existing AKS environment to recipe `1.0.7`](https://github.com/microsoft/MicroHack/blob/main/03-Azure/01-01-App%20Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md#update-an-existing-aks-environment-to-recipe-107). -Verify that the registration references `postgres-azure-flex:1.0.7`, then rerun the -unchanged AKS `rad deploy iac/app.bicep` command from Stage 3. Retries use the same -`1.0.7` server name. +For this fresh deployment, the registration must reference `postgres-azure-flex:1.0.8`. +If it does not, return to Challenge 05: publish the `1.0.8` recipes in Stage 2 and rerun +the Stage 3 `rad deploy iac/aks-env.bicep` command with the `1.0.8` PostgreSQL recipe +path. Verify the registration before retrying the unchanged AKS +`rad deploy iac/app.bicep` command from Stage 3 here. Retries use the same `1.0.8` +server name. -If the registration already uses `1.0.7`, check that the environment's Azure provider +If the registration already uses `1.0.8`, check that the environment's Azure provider scope is not shared with another workshop that uses the same Radius resource names. ### The deployment reached the wrong platform diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-09/solution-09.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-09/solution-09.md index 380ba3f74..0987d4fda 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-09/solution-09.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-09/solution-09.md @@ -533,7 +533,7 @@ name. ```bash export AZURE_SUBSCRIPTION="${AZURE_SUBSCRIPTION:-}" -export RESOURCE_GROUP="${RESOURCE_GROUP:-rg-adaptive-apps}" +export RESOURCE_GROUP="${RESOURCE_GROUP:-}" export AKS_NAME="aks-adaptive-apps" export OPENAI_RESOURCE_GROUP="" export OPENAI_ACCOUNT="" @@ -591,7 +591,9 @@ test "$WORKLOAD_IDENTITY_ENABLED" = "true" ```powershell $env:AZURE_SUBSCRIPTION = "" -$env:RESOURCE_GROUP = "rg-adaptive-apps" +if (-not $env:RESOURCE_GROUP) { + $env:RESOURCE_GROUP = "" +} $env:AKS_NAME = "aks-adaptive-apps" $env:OPENAI_RESOURCE_GROUP = "" $env:OPENAI_ACCOUNT = "" From 22146cb45c6359fcdc0c10505261de6f6920d3c0 Mon Sep 17 00:00:00 2001 From: Dylan de Jong Date: Thu, 22 Oct 2026 20:47:52 +0200 Subject: [PATCH 3/6] Revise troubleshooting for Kubernetes Job in PostgreSQL recipes Updated troubleshooting section for Kubernetes Job properties and added details on handling line endings. --- .../walkthrough/challenge-05/solution-05.md | 83 +++++++------------ 1 file changed, 30 insertions(+), 53 deletions(-) diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md index e9e30ce00..25a052a0b 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md @@ -122,59 +122,6 @@ Derive that name from a compile-time variable rather than from `tradingDb.name`, compile to a runtime `reference('tradingDb').name` call that cannot resolve. `resources/validate-postgres-recipes.sh` guards all of these. -### Kubernetes resources created by a recipe - -The PostgreSQL recipes also create a Kubernetes Job that applies the trading schema. Two -properties of that Job are easy to get wrong and fail silently. - -**A Job's `spec.template` is immutable.** Kubernetes rejects an update that changes the pod -template of an existing Job with `spec.template: field is immutable`, so the Job has to be -replaced rather than updated. That only happens if its *name* changes, which means the name -must be derived from everything the pod template embeds — the script, the image tag and the -name of the Secret holding the password, not just the schema SQL. The recipes hash all of -those into `schemaRevision`. Radius then creates the new Job and garbage-collects the old one -through `result.resources`. - -**Line endings reach the container verbatim.** Bicep preserves the on-disk line endings of -`loadTextContent()` and of `'''…'''` multi-line strings, so a CRLF checkout embeds carriage -returns in the Job's shell script. `sh` does not treat CR as whitespace, so `do` stops -being the `do` keyword and the container exits immediately with -`syntax error: unexpected word`. Because the recipe does not wait for the Job to succeed, the -deployment still reports success and the database is simply left empty. The repository pins -these files to LF in `.gitattributes`, and the recipes additionally strip carriage returns -with `replace(..., '\r', '')` so they stay correct on any checkout. - -Verify the Job actually completed rather than trusting the deployment result: - -```bash -kubectl get jobs -n -``` - -`COMPLETIONS` must read `1/1`. - -### Globally unique Azure PostgreSQL names - -Azure Database for PostgreSQL Flexible Server names are globally unique. Separate -workshops commonly use the same Radius resource ID, for example the same group, -application, and `trading-db` resource names on different clusters. Recipe version -`1.0.8` retains the deterministic server name scoped to the target Azure resource group: - -```bicep -var serverName = 'pg-${uniqueString(resourceGroup().id, context.resource.id)}' -``` - -`resourceGroup().id` contains both the subscription ID and resource-group name, so -identical Radius resource IDs in different Azure resource groups receive different -names. Different Radius resources in one resource group also receive different names, -and retries with the same inputs reuse the same name. The result is `pg-` plus 13 -lowercase alphanumeric characters, which satisfies the Flexible Server naming rules. - -Give every workshop its own Azure resource group in the AKS environment's Azure -provider scope. Hosted Console labs already do this. Sharing an Azure resource group -between workshops is not supported by this recipe; a design that needs it must add a -stable, explicit workshop identifier to the hash. Never use a timestamp or another -value that changes on every deployment. - ## Manual tutorial, Stage 1: Author the Azure SQL recipe Open `iac/recipes/sql-server.bicep` and walk through: @@ -675,3 +622,33 @@ environment definition, and verifies the registered recipes. It uses `WORKSHOP_RECIPE_VERSION=1.0.8` by default and reuses an existing version tag instead of overwriting it. `k3s` also needs `AZURE_SUBSCRIPTION`, `RESOURCE_GROUP`, and `ACR_NAME` because it publishes the pinned workshop recipe before registration when needed. + +### Troubleshooting: Kubernetes resources created by a recipe + +The PostgreSQL recipes also create a Kubernetes Job that applies the trading schema. Two +properties of that Job are easy to get wrong and fail silently. + +**A Job's `spec.template` is immutable.** Kubernetes rejects an update that changes the pod +template of an existing Job with `spec.template: field is immutable`, so the Job has to be +replaced rather than updated. That only happens if its *name* changes, which means the name +must be derived from everything the pod template embeds — the script, the image tag and the +name of the Secret holding the password, not just the schema SQL. The recipes hash all of +those into `schemaRevision`. Radius then creates the new Job and garbage-collects the old one +through `result.resources`. + +**Line endings reach the container verbatim.** Bicep preserves the on-disk line endings of +`loadTextContent()` and of `'''…'''` multi-line strings, so a CRLF checkout embeds carriage +returns in the Job's shell script. `sh` does not treat CR as whitespace, so `do` stops +being the `do` keyword and the container exits immediately with +`syntax error: unexpected word`. Because the recipe does not wait for the Job to succeed, the +deployment still reports success and the database is simply left empty. The repository pins +these files to LF in `.gitattributes`, and the recipes additionally strip carriage returns +with `replace(..., '\r', '')` so they stay correct on any checkout. + +Verify the Job actually completed rather than trusting the deployment result: + +```bash +kubectl get jobs -n +``` + +`COMPLETIONS` must read `1/1`. From f594de15f33c83d9a25effc6678d9d4ed0d7b00e Mon Sep 17 00:00:00 2001 From: Dylan de Jong Date: Thu, 22 Oct 2026 20:51:55 +0200 Subject: [PATCH 4/6] Remove optional platforms section from solution-05.md Removed optional platforms section from the tutorial. --- .../walkthrough/challenge-05/solution-05.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md index 25a052a0b..f456c4a7d 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-05/solution-05.md @@ -592,12 +592,6 @@ kubectl port-forward service/dashboard \ Open , navigate to the environment, and inspect **Recipes**. -## Optional platforms - -If Azure Local or Arc-enabled Kubernetes replaces K3s, use `iac/local-env.bicep` for -in-cluster implementations unless the platform team intentionally provides managed -services. Always use a distinct workspace and translate context and environment names -consistently. ## Optional automation From 41a0310fa57148a0fba9df7b2022dd36373eb530 Mon Sep 17 00:00:00 2001 From: Dylan de Jong Date: Thu, 22 Oct 2026 20:54:08 +0200 Subject: [PATCH 5/6] Modify section title in solution-06.md Updated section title for clarity. --- .../04-adaptive-apps/walkthrough/challenge-06/solution-06.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md index f73c81728..251ca079f 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md @@ -97,7 +97,7 @@ API on localhost; it does not expose application HTTP, HTTPS, MQTT, or SSH ports Each workspace targets an independent Radius control plane. Switching `kubectl` context without switching the Radius workspace can deploy to the wrong platform. -## Stage 1: Inspect the invariant application model +## Stage 1: Inspect the application model Open `iac/app.bicep`. The core model declares: From f79daf260500a40f52501b0f8897bd76d7e9e830 Mon Sep 17 00:00:00 2001 From: Dylan de Jong Date: Thu, 22 Oct 2026 20:56:55 +0200 Subject: [PATCH 6/6] Update solution-06.md with Radius validation steps Added instructions to validate graph, resources, and workloads using Radius. --- .../04-adaptive-apps/walkthrough/challenge-06/solution-06.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md index 251ca079f..5407ef2b6 100644 --- a/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md +++ b/03-Azure/01-01-App Innovation/04-adaptive-apps/walkthrough/challenge-06/solution-06.md @@ -288,6 +288,8 @@ on a trusted, single-user workstation or devcontainer host. ### 4. Validate graph, resources, and workloads +Radius stores the application Graph. resources and workload types. Let's check if we can retrieve these from the Radius control plane. + **Bash:** ```bash