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
Original file line number Diff line number Diff line change
Expand Up @@ -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.0`.
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.
Expand Down Expand Up @@ -120,9 +139,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.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.
- 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`.
Expand All @@ -140,6 +161,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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.8`,
then retry the unchanged deployment.

### Deployment and validation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ resource tradingDb 'Radius.Resources/postgreSqlDatabases@2025-08-01-preview' = {
properties: {
environment: environment
application: tradingApp.id
size: 'S'
size: 'L'
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,15 @@ var skuBySize = {
tier: 'Burstable'
}
L: {
name: 'Standard_D2s_v3'
name: 'Standard_D4ds_v5'
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' {
Expand Down Expand Up @@ -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' {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.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
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -158,15 +163,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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ 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
exit 1
}
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}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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")"
Expand Down Expand Up @@ -216,27 +221,37 @@ 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
exit 1
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
exit 1
}
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -62,6 +73,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
Expand Down
Loading
Loading