From efafea47e3b2b925994dc5dc9907462cf443b267 Mon Sep 17 00:00:00 2001 From: "Tom Meschter (from Dev Box)" Date: Fri, 5 Jun 2026 15:58:06 -0700 Subject: [PATCH 1/2] feat: replace azd context detect apply verify sequence with script (#2495) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../references/aspire-troubleshooting.md | 59 ++++ .../skills/azure-prepare/references/aspire.md | 295 ++---------------- .../azure-prepare/references/azure-context.md | 45 +-- .../references/scripts/set-azd-context.ps1 | 144 +++++++++ .../references/scripts/set-azd-context.sh | 132 ++++++++ 5 files changed, 366 insertions(+), 309 deletions(-) create mode 100644 plugin/skills/azure-prepare/references/aspire-troubleshooting.md create mode 100644 plugin/skills/azure-prepare/references/scripts/set-azd-context.ps1 create mode 100644 plugin/skills/azure-prepare/references/scripts/set-azd-context.sh diff --git a/plugin/skills/azure-prepare/references/aspire-troubleshooting.md b/plugin/skills/azure-prepare/references/aspire-troubleshooting.md new file mode 100644 index 000000000..6f1f4372f --- /dev/null +++ b/plugin/skills/azure-prepare/references/aspire-troubleshooting.md @@ -0,0 +1,59 @@ +# Aspire Troubleshooting + +## Non-interactive `azd init` prompts + +- `no default response for prompt 'Enter a unique environment name:'` → include `-e `. +- `no default response for prompt 'How do you want to initialize your app?'` → include `--from-code`. + +```bash +ENV_NAME="$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | tr ' _' '-')-dev" +azd init --from-code -e "$ENV_NAME" +``` + +## No AppHost detected + +1. Verify AppHost project exists: `find . -name "*.AppHost.csproj"` +2. Check project builds: `dotnet build` +3. Ensure Aspire.Hosting is referenced by the AppHost project. + +## Unsupported resource type + +If manifest generation fails with `unsupported resource type`, the AppHost contains custom Aspire resources that azd cannot deploy. + +1. Do **not** modify source code to add `.ExcludeFromManifest()` or otherwise suppress the error. +2. Do **not** proceed with deployment. +3. Record a blocker: "AppHost contains custom Aspire resource types not supported for Azure deployment." +4. Tell the user the application targets local development or custom tooling, not Azure deployment. + +## Azure Functions secret storage + +When Aspire Functions use `.WithHostStorage(storage)`, Azure Functions secret/key management cannot use identity-based storage URIs. Before `azd up`, add file-based secret storage: + +```csharp +var functions = builder.AddAzureFunctionsProject("functions") + .WithHostStorage(storage) + .WithEnvironment("AzureWebJobsSecretStorageType", "Files"); +``` + +If generated infrastructure must be edited directly, ensure the Functions container app has: + +```bicep +{ + name: 'AzureWebJobsSecretStorageType' + value: 'Files' +} +``` + +## Wrong subscription + +The Azure CLI and azd keep separate contexts. After `azd init --from-code`, immediately run: + +```bash +./scripts/set-azd-context.sh +``` + +Use the PowerShell helper on Windows: + +```powershell +.\scripts\set-azd-context.ps1 -SubscriptionId -Location -EnvironmentName +``` diff --git a/plugin/skills/azure-prepare/references/aspire.md b/plugin/skills/azure-prepare/references/aspire.md index 96361b131..bc663b229 100644 --- a/plugin/skills/azure-prepare/references/aspire.md +++ b/plugin/skills/azure-prepare/references/aspire.md @@ -11,10 +11,6 @@ Guidance for preparing .NET Aspire applications for Azure deployment. **📖 For detailed AZD workflow:** See [recipes/azd/aspire.md](recipes/azd/aspire.md) -## What is .NET Aspire? - -.NET Aspire is an opinionated, cloud-ready stack for building observable, production-ready distributed applications. Aspire projects use an AppHost orchestrator to define and configure the application's components, services, and dependencies. - ## Detection A .NET Aspire project is identified by: @@ -25,17 +21,6 @@ A .NET Aspire project is identified by: | `Aspire.Hosting` package | Core Aspire hosting package reference | | `Aspire.Hosting.AppHost` | Alternative Aspire hosting package | -**Example project structure:** -``` -orleans-voting/ -├── OrleansVoting.sln -├── OrleansVoting.AppHost/ -│ └── OrleansVoting.AppHost.csproj ← AppHost indicator -├── OrleansVoting.Web/ -├── OrleansVoting.Api/ -└── OrleansVoting.Grains/ -``` - ## Azure Preparation Workflow ### Step 1: Detection @@ -52,7 +37,7 @@ grep -r "Aspire.Hosting" . --include="*.csproj" ### ⛔ Step 1a: Pre-Check for Custom/Non-Deployable Resources (MANDATORY) -**Before running `azd init --from-code`, scan the AppHost source code to understand whether the app may contain local-only custom resources.** +Before `azd init --from-code`, scan AppHost source for local-only custom resources: ```bash # Find the AppHost project and scan only its source directory @@ -61,54 +46,24 @@ APPHOST_DIR=$(dirname "$APPHOST_PROJECT") grep -r "ExcludeFromManifest" "$APPHOST_DIR" --include="*.cs" | head -20 ``` -**PowerShell:** -```powershell -# Find the AppHost project and scan only its source directory -$appHostProject = Get-ChildItem -Recurse -Filter "*.AppHost.csproj" | Select-Object -First 1 -$appHostDir = $appHostProject.DirectoryName -Get-ChildItem -Path $appHostDir -Recurse -Filter "*.cs" | Select-String "ExcludeFromManifest" | Select-Object -First 20 -``` - -This scan is informational. `.ExcludeFromManifest()` can appear alongside deployable resources, so a positive match does **not** immediately block deployment. What matters is the final `azure.yaml` output after `azd init --from-code` completes: +This scan is informational. A positive match does **not** immediately block deployment; final `azure.yaml` output matters: - If `azd init` **fails** with `unsupported resource type` → see Step 2 error guidance below. - If `azd init` **succeeds** but `azure.yaml` has an empty or missing `services` section → see Step 4a below. -> 💡 **Why scan early:** Knowing that `.ExcludeFromManifest()` is present gives useful context when azd errors or generates an empty manifest — it confirms the app intentionally targets local development rather than Azure deployment. - ### Step 2: Initialize with azd **CRITICAL: For Aspire projects, use `azd init --from-code -e ` instead of creating azure.yaml manually.** **⚠️ ALWAYS include the `-e ` flag:** Without it, `azd init` will fail in non-interactive environments (agents, CI/CD) with the error: `no default response for prompt 'Enter a unique environment name:'` -The `--from-code` flag: -- Auto-detects the AppHost orchestrator -- Reads the Aspire service definitions -- Generates appropriate `azure.yaml` and infrastructure -- Works in non-interactive/CI environments when combined with `-e` flag - ```bash # Non-interactive initialization for Aspire projects (REQUIRED for agents) ENV_NAME="$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | tr ' _' '-')-dev" azd init --from-code -e "$ENV_NAME" ``` -**Why both flags are required:** -- `--from-code`: Tells azd to detect the AppHost automatically (no "How do you want to initialize?" prompt) -- `-e `: Provides environment name upfront (no "Enter environment name:" prompt) -- Together, they enable fully non-interactive operation essential for automation, agents, and CI/CD pipelines - -**⛔ If `azd init --from-code` fails with "unsupported resource type":** - -This error means the AppHost contains custom Aspire resource types that azd cannot process for Azure deployment: - -1. ⛔ **Do NOT attempt to fix this error by modifying source code** — do not add `.ExcludeFromManifest()` calls or otherwise patch the AppHost -2. ⛔ **Do NOT proceed with deployment** — the application is designed for local development only -3. ✅ Record a blocker: "AppHost contains custom Aspire resource types (`unsupported resource type`) that cannot be deployed to Azure" -4. ✅ Inform the user: this application uses custom Aspire resource authoring patterns intended for local tooling, not cloud deployment - -> ⚠️ **Why modifying source code is forbidden:** Adding `.ExcludeFromManifest()` may suppress the error and allow `azd init` to succeed, but the deployment outcome will not reflect the application's actual intent. The custom resources are deliberately designed to be local-only. +**If `azd init --from-code` fails with `unsupported resource type`:** do not patch AppHost source or proceed. Record a blocker that custom Aspire resource types cannot be deployed to Azure. See [Aspire troubleshooting](aspire-troubleshooting.md). ### Step 3: Configure Subscription and Location @@ -116,20 +71,14 @@ This error means the AppHost contains custom Aspire resource types that azd cann > > **DO NOT** skip this step or delay it until validation. The `azd init` command creates an environment but does NOT inherit the Azure CLI's subscription. If you skip this step, azd will use its own default subscription, which may differ from the user's confirmed choice. -**Set the subscription and location immediately after initialization:** +**Set and verify the subscription/location immediately after initialization** with the AZD context helper. It detects existing/default context, sets `AZURE_SUBSCRIPTION_ID` before `AZURE_LOCATION`, verifies both values, and emits `key=value` lines plus a summary: ```bash -# Set the user-confirmed subscription ID -azd env set AZURE_SUBSCRIPTION_ID - -# Set the location -azd env set AZURE_LOCATION +./scripts/set-azd-context.sh ``` -**Verify the configuration:** - -```bash -azd env get-values +```powershell +.\scripts\set-azd-context.ps1 -SubscriptionId -Location -EnvironmentName ``` Confirm that `AZURE_SUBSCRIPTION_ID` and `AZURE_LOCATION` match the user's confirmed choices from [Azure Context](azure-context.md). @@ -146,83 +95,36 @@ Confirm that `AZURE_SUBSCRIPTION_ID` and `AZURE_LOCATION` match the user's confi ### ⛔ Step 4a: Validate Generated Output -**MANDATORY: After `azd init --from-code` completes, verify the generated `azure.yaml` contains deployable services.** +Verify generated `azure.yaml` contains a non-empty `services:` map: ```bash # Check if azure.yaml has a non-empty services section cat azure.yaml ``` -**If the `services` section is empty or missing:** The AppHost has no deployable resources. This happens when all resources use `.ExcludeFromManifest()` (e.g., custom resource demonstrations, local-only tooling). In this case: - -1. ⛔ **Do NOT proceed with deployment** — there is nothing to deploy -2. ✅ Keep the plan status in a valid state (for example, leave it as **Planning**) and record a blocker in the plan body with the reason: "Application contains only custom/demo Aspire resources with no Azure-deployable services" -3. ✅ Inform the user that this application is designed for local development and cannot be meaningfully deployed to Azure -4. ⛔ Do NOT manually create Bicep, Dockerfiles, or azure.yaml to work around this — the absence of services is the correct result - -**Example generated azure.yaml:** -```yaml -name: orleans-voting -# metadata section is auto-generated by azd init --from-code +If the `services` section is empty or missing, the AppHost has no deployable resources: -services: - web: - project: ./OrleansVoting.Web - language: dotnet - host: containerapp - - api: - project: ./OrleansVoting.Api - language: dotnet - host: containerapp -``` +1. Do **not** proceed or manually create Bicep, Dockerfiles, or azure.yaml. +2. Record a blocker: "Application contains only custom/demo Aspire resources with no Azure-deployable services." +3. Inform the user that this app is local-only. ### ⛔ Step 4b: Fix Azure Functions Secret Storage (MANDATORY for Aspire + Functions) -**MANDATORY: After `azd init --from-code` succeeds, check if the AppHost contains Azure Functions and fix secret storage configuration.** - -This step **MUST** run BEFORE `azd up` or `azd provision`. Skipping it causes a runtime failure: `Secret initialization from Blob storage failed`. - -**1. Detect Azure Functions in the AppHost:** +For Aspire + Functions, ensure file-based secret storage before `azd up`: ```bash APPHOST_DIR=$(dirname "$(find . -name '*.AppHost.csproj' | head -1)") grep -n "AddAzureFunctionsProject" "$APPHOST_DIR"/*.cs ``` -**PowerShell:** -```powershell -$appHostDir = (Get-ChildItem -Recurse -Filter "*.AppHost.csproj" | Select-Object -First 1).DirectoryName -Get-ChildItem -Path $appHostDir -Filter "*.cs" | Select-String "AddAzureFunctionsProject" -``` - -**If `AddAzureFunctionsProject` is NOT found → skip this step.** - -**2. Check if `AzureWebJobsSecretStorageType` is already configured:** +If `AddAzureFunctionsProject` is absent, skip. If present, check whether `AzureWebJobsSecretStorageType` already exists: ```bash grep -n "AzureWebJobsSecretStorageType" "$APPHOST_DIR"/*.cs ``` -**PowerShell:** -```powershell -Get-ChildItem -Path $appHostDir -Filter "*.cs" | Select-String "AzureWebJobsSecretStorageType" -``` - -**If already present → skip this step.** +If absent, add it immediately after `.WithHostStorage(...)`: -**3. Add the environment variable to the Functions builder chain:** - -Use the `edit` tool to add `.WithEnvironment("AzureWebJobsSecretStorageType", "Files")` to the `AddAzureFunctionsProject` builder chain in the AppHost source file. - -**Before:** -```csharp -var functions = builder.AddAzureFunctionsProject("functions") - .WithHostStorage(storage) - .WithReference(queues); -``` - -**After:** ```csharp var functions = builder.AddAzureFunctionsProject("functions") .WithHostStorage(storage) @@ -230,11 +132,7 @@ var functions = builder.AddAzureFunctionsProject("function .WithReference(queues); ``` -> 💡 **Tip:** Place `.WithEnvironment(...)` immediately after `.WithHostStorage(...)` for clarity. - -> ⚠️ **Why this is required:** When Aspire uses `WithHostStorage(storage)`, it configures identity-based storage URIs (e.g., `AzureWebJobsStorage__blobServiceUri`). Azure Functions' secret/key manager does **not** support these identity-based URIs — it requires either a connection string or file-based storage. Setting `AzureWebJobsSecretStorageType=Files` switches to file-system key storage, bypassing the incompatible blob dependency. - -See [aspire-functions-secrets reference](services/functions/aspire-containerapps.md) for additional details. +See [aspire-functions-secrets reference](services/functions/aspire-containerapps.md) and [Aspire troubleshooting](aspire-troubleshooting.md) for details. ## Flags Reference @@ -252,166 +150,13 @@ See [aspire-functions-secrets reference](services/functions/aspire-containerapps ENV_NAME="$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | tr ' _' '-')-dev" azd init --from-code -e "$ENV_NAME" -# 2. IMMEDIATELY set the user-confirmed subscription -azd env set AZURE_SUBSCRIPTION_ID - -# 3. Set the location -azd env set AZURE_LOCATION - -# 4. Verify configuration -azd env get-values +# 2. IMMEDIATELY detect, set subscription first, set location, and verify +./scripts/set-azd-context.sh "$ENV_NAME" ``` -## Common Aspire Samples - -| Sample | Repository | Notes | -|--------|------------|-------| -| orleans-voting | [dotnet/aspire-samples](https://github.com/dotnet/aspire-samples/tree/main/samples/orleans-voting) | Orleans cluster with voting app | -| AspireYarp | [dotnet/aspire-samples](https://github.com/dotnet/aspire-samples/tree/main/samples/AspireYarp) | YARP reverse proxy | -| AspireWithDapr | [dotnet/aspire-samples](https://github.com/dotnet/aspire-samples/tree/main/samples/AspireWithDapr) | Dapr integration | -| eShop | [dotnet/eShop](https://github.com/dotnet/eShop) | Reference microservices app | - ## Troubleshooting -### Error: "no default response for prompt 'Enter a unique environment name:'" - -**Cause:** Missing `-e` flag when running `azd init --from-code` in non-interactive environment -**Solution:** Always include the `-e ` flag - -```bash -# ❌ Wrong - fails in non-interactive environments (agents, CI/CD) -azd init --from-code - -# ✅ Correct - provides environment name upfront -ENV_NAME="$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | tr ' _' '-')-dev" -azd init --from-code -e "$ENV_NAME" -``` - -**Important:** This error typically occurs when: -- Running in an agent or automation context -- No TTY is available for interactive prompts -- The `-e` flag was omitted - -### Error: "no default response for prompt 'How do you want to initialize your app?'" - -**Cause:** Missing `--from-code` flag -**Solution:** Add `--from-code` to the `azd init` command - -```bash -# ❌ Wrong - requires interactive prompt -azd init -e "my-env" - -# ✅ Correct - auto-detects AppHost -azd init --from-code -e "my-env" -``` - -### No AppHost detected - -**Symptoms:** `azd init --from-code` doesn't find the AppHost - -**Solutions:** -1. Verify AppHost project exists: `find . -name "*.AppHost.csproj"` -2. Check project builds: `dotnet build` -3. Ensure Aspire.Hosting package is referenced in AppHost project - -### Error: "unsupported resource type" during manifest generation - -**Symptoms:** `azd init --from-code` fails with output like: -``` -error: unsupported resource type: -``` -or the manifest generation step errors on child resources (e.g., ClockHand, or other custom resource types defined in the AppHost). - -**Cause:** The AppHost contains custom Aspire resource types that azd cannot convert to Azure deployable resources. These custom types are typically: -- Demonstration resources showing developers how to build Aspire extensions for local tooling -- Resources that wrap local services without Azure equivalents -- Custom child resources (e.g., subcomponents of a custom Aspire integration) - -**Resolution:** - -1. ⛔ **Do NOT attempt to fix this error by modifying source code** — do not add `.ExcludeFromManifest()` calls or otherwise patch the AppHost -2. ⛔ **Do NOT proceed with deployment** — this is a deployment blocker, not a recoverable error -3. ✅ Record a blocker in the deployment plan: "AppHost contains custom Aspire resource types not supported for Azure deployment (unsupported resource type)" -4. ✅ Inform the user that this application is designed for local development and cannot be meaningfully deployed to Azure - -> ⚠️ **Why this is a hard stop:** Custom resource types that produce "unsupported resource type" errors are intentionally not deployable. Adding `.ExcludeFromManifest()` to suppress the error may allow `azd init` to succeed, but the resulting deployment would not represent the application's actual functionality. - -### Azure Functions: Secret initialization from Blob storage failed - -**Symptoms:** Azure Functions app fails at startup with error: -``` -System.InvalidOperationException: Secret initialization from Blob storage failed due to missing both -an Azure Storage connection string and a SAS connection uri. -``` - -**Cause:** When using `AddAzureFunctionsProject` with `WithHostStorage(storage)`, Aspire configures identity-based storage access (managed identity). However, Azure Functions' internal secret management does not support identity-based URIs and requires file-based secret storage for Container Apps deployments. - -**Solution:** Add `AzureWebJobsSecretStorageType=Files` environment variable to the Functions resource in the AppHost **before running `azd up`**: - -```csharp -var functions = builder.AddAzureFunctionsProject("functions") - .WithReference(queues) - .WithReference(blobs) - .WaitFor(storage) - .WithRoleAssignments(storage, ...) - .WithHostStorage(storage) - .WithEnvironment("AzureWebJobsSecretStorageType", "Files") // Required for Container Apps - .WithUrlForEndpoint("http", u => u.DisplayText = "Functions App"); -``` - -> 💡 **Why this is required:** -> - `WithHostStorage(storage)` sets identity-based URIs like `AzureWebJobsStorage__blobServiceUri` -> - This is correct and secure for runtime storage operations -> - However, Functions' secret/key management doesn't support these URIs -> - File-based secrets are mandatory for Container Apps deployments - -> ⚠️ **Important:** This is required when: -> - Using `AddAzureFunctionsProject` in Aspire -> - Using `WithHostStorage()` with identity-based storage -> - Deploying to Azure Container Apps (the default for Aspire Functions) - -**Generated Infrastructure Note:** - -If you need to modify the generated Container Apps infrastructure directly, ensure the Functions container app has this environment variable: - -```bicep -resource functionsContainerApp 'Microsoft.App/containerApps@2024-03-01' = { - properties: { - template: { - containers: [ - { - env: [ - { - name: 'AzureWebJobsSecretStorageType' - value: 'Files' - } - // ... other environment variables - ] - } - ] - } - } -} -``` - -### Error: azd uses wrong subscription despite user confirmation - -**Symptoms:** `azd provision --preview` shows a different subscription than the one the user confirmed - -**Cause:** The `AZURE_SUBSCRIPTION_ID` was not set immediately after `azd init --from-code`. The Azure CLI and azd can have different default subscriptions. - -**Solution:** Always set the subscription immediately after initialization: - -```bash -# After azd init --from-code completes: -azd env set AZURE_SUBSCRIPTION_ID -azd env set AZURE_LOCATION - -# Verify before proceeding: -azd env get-values -``` - -**Prevention:** Follow the complete initialization sequence in the [Flags Reference](#azd-init-for-aspire) section above. +See [Aspire troubleshooting](aspire-troubleshooting.md) for non-interactive `azd init`, missing AppHost, unsupported resource type, Azure Functions secret storage, and wrong-subscription fixes. ## References @@ -424,7 +169,7 @@ azd env get-values After `azd init --from-code`: 1. Review generated `azure.yaml` and `infra/` files (if present) -2. Set AZURE_SUBSCRIPTION_ID and AZURE_LOCATION with `azd env set` +2. Set and verify AZURE_SUBSCRIPTION_ID and AZURE_LOCATION with [set-azd-context.sh](scripts/set-azd-context.sh) or [set-azd-context.ps1](scripts/set-azd-context.ps1) 3. Customize infrastructure as needed 4. Proceed to **azure-validate** skill 5. Deploy with **azure-deploy** skill diff --git a/plugin/skills/azure-prepare/references/azure-context.md b/plugin/skills/azure-prepare/references/azure-context.md index 8d20a5437..a4303ca60 100644 --- a/plugin/skills/azure-prepare/references/azure-context.md +++ b/plugin/skills/azure-prepare/references/azure-context.md @@ -6,16 +6,14 @@ Detect and confirm Azure subscription and location before generating artifacts. ## Step 1: Check for Existing AZD Environment -If the project already uses AZD, check for an existing environment with values already set: +Use the context helper to detect any selected AZD environment, existing values, azd defaults, and Azure CLI fallback subscription. It emits `key=value` lines plus a summary, so do not re-parse raw azd output: ```bash -azd env list +./scripts/set-azd-context.sh --detect-only [environment-name] ``` -**If an environment is selected** (marked with `*`), check its values: - -```bash -azd env get-values +```powershell +.\scripts\set-azd-context.ps1 -DetectOnly [-EnvironmentName ] ``` If `AZURE_SUBSCRIPTION_ID` and `AZURE_LOCATION` are already set, use `ask_user` to confirm reuse: @@ -39,13 +37,7 @@ If user confirms → skip to **Record in Plan**. Otherwise → continue to Step ## Step 2: Detect Defaults -Check for user-configured defaults: - -```bash -azd config get defaults -``` - -Returns JSON with any configured defaults: +The [context helper](scripts/set-azd-context.sh) / [PowerShell helper](scripts/set-azd-context.ps1) already checks `azd config get defaults` and falls back to `az account show --query "{name:name, id:id}" -o json`. Defaults appear as: ```json { "subscription": "25fd0362-aa79-488b-b37b-d6e892009fdf", @@ -55,11 +47,6 @@ Returns JSON with any configured defaults: Use these as **recommended** values if present. -If no defaults, fall back to az CLI: -```bash -az account show --query "{name:name, id:id}" -o json -``` - ## Step 3: Confirm Subscription with User Use `ask_user` with the **actual subscription name and ID**: @@ -152,14 +139,8 @@ After confirmation, record in `.azure/deployment-plan.md`: # 1. Run azd init azd init --from-code -e --no-prompt -# 2. IMMEDIATELY set the user-confirmed subscription -azd env set AZURE_SUBSCRIPTION_ID - -# 3. Set the location -azd env set AZURE_LOCATION - -# 4. Verify -azd env get-values +# 2. IMMEDIATELY detect, set subscription first, set location, and verify +./scripts/set-azd-context.sh ``` **For non-Aspire projects using `azd env new`:** @@ -168,16 +149,12 @@ azd env get-values # 1. Create environment azd env new --no-prompt -# 2. IMMEDIATELY set the user-confirmed subscription -azd env set AZURE_SUBSCRIPTION_ID - -# 3. Set the location -azd env set AZURE_LOCATION - -# 4. Verify -azd env get-values +# 2. IMMEDIATELY detect, set subscription first, set location, and verify +./scripts/set-azd-context.sh ``` +PowerShell: `.\scripts\set-azd-context.ps1 -SubscriptionId -Location -EnvironmentName `. + **Why this is critical:** - `az account show` returns the Azure CLI's default subscription - `azd` maintains its own configuration with potentially different defaults diff --git a/plugin/skills/azure-prepare/references/scripts/set-azd-context.ps1 b/plugin/skills/azure-prepare/references/scripts/set-azd-context.ps1 new file mode 100644 index 000000000..05fd6eab1 --- /dev/null +++ b/plugin/skills/azure-prepare/references/scripts/set-azd-context.ps1 @@ -0,0 +1,144 @@ +<# +.SYNOPSIS + Detects, applies, and verifies azd subscription/location context. +.DESCRIPTION + Emits machine-readable key=value lines followed by a human-readable summary. +.PARAMETER SubscriptionId + User-confirmed Azure subscription ID to set in the azd environment. +.PARAMETER Location + User-confirmed Azure location to set in the azd environment. +.PARAMETER EnvironmentName + Optional azd environment name to select before detecting or setting values. +.PARAMETER DetectOnly + Detect current azd/default/Azure CLI context without changing azd values. +.EXAMPLE + .\set-azd-context.ps1 -SubscriptionId -Location -EnvironmentName +.EXAMPLE + .\set-azd-context.ps1 -DetectOnly -EnvironmentName +#> +param( + [string]$SubscriptionId, + [string]$Location, + [string]$EnvironmentName, + [switch]$DetectOnly +) + +$ErrorActionPreference = 'Stop' + +if (-not $DetectOnly -and (-not $SubscriptionId -or -not $Location)) { + throw 'Usage: .\set-azd-context.ps1 -SubscriptionId -Location [-EnvironmentName ] or .\set-azd-context.ps1 -DetectOnly [-EnvironmentName ]' +} + +function Convert-AzdValue { + param([string]$Value) + if ($null -eq $Value) { return '' } + return $Value.Trim().Trim('"').Trim("'") +} + +function Get-AzdContextValues { + $result = @{ + SubscriptionId = '' + Location = '' + } + + $values = azd env get-values 2>$null + if ($LASTEXITCODE -ne 0) { + return $result + } + + foreach ($line in $values) { + if (-not $line -or -not $line.Contains('=')) { continue } + $name, $value = $line.Split('=', 2) + $cleanValue = Convert-AzdValue $value + switch ($name) { + 'AZURE_SUBSCRIPTION_ID' { $result.SubscriptionId = $cleanValue } + 'AZURE_LOCATION' { $result.Location = $cleanValue } + } + } + + return $result +} + +$azdEnvironment = $EnvironmentName +if ($azdEnvironment) { + azd env select $azdEnvironment | Out-Null +} + +$azdEnvList = azd env list 2>$null +if (-not $azdEnvironment -and $LASTEXITCODE -eq 0) { + $selected = $azdEnvList | Where-Object { $_ -match '^\*\s+' } | Select-Object -First 1 + if ($selected -and $selected -match '^\*\s+(\S+)') { + $azdEnvironment = $Matches[1] + } +} + +$existing = Get-AzdContextValues +$existingSubscriptionId = $existing.SubscriptionId +$existingLocation = $existing.Location + +$defaultSubscriptionId = '' +$defaultLocation = '' +$defaultsJson = azd config get defaults 2>$null +if ($LASTEXITCODE -eq 0 -and $defaultsJson) { + $defaults = ($defaultsJson | Out-String | ConvertFrom-Json) + $defaultSubscriptionId = $defaults.subscription + $defaultLocation = $defaults.location +} + +$azSubscriptionName = '' +$azSubscriptionId = '' +$accountJson = az account show --query "{name:name, id:id}" -o json 2>$null +if ($LASTEXITCODE -eq 0 -and $accountJson) { + $account = ($accountJson | Out-String | ConvertFrom-Json) + $azSubscriptionName = $account.name + $azSubscriptionId = $account.id +} + +$verifiedSubscriptionId = '' +$verifiedLocation = '' +if (-not $DetectOnly) { + azd env set AZURE_SUBSCRIPTION_ID $SubscriptionId | Out-Null + azd env set AZURE_LOCATION $Location | Out-Null + + $verified = Get-AzdContextValues + $verifiedSubscriptionId = $verified.SubscriptionId + $verifiedLocation = $verified.Location + + if ($verifiedSubscriptionId -ne $SubscriptionId -or $verifiedLocation -ne $Location) { + Write-Output 'status=failed' + Write-Output "requested_subscription_id=$SubscriptionId" + Write-Output "requested_location=$Location" + Write-Output "verified_subscription_id=$verifiedSubscriptionId" + Write-Output "verified_location=$verifiedLocation" + throw 'azd context verification failed.' + } +} else { + $verifiedSubscriptionId = $existingSubscriptionId + $verifiedLocation = $existingLocation +} + +$status = if ($DetectOnly) { 'detected' } else { 'success' } +Write-Output "status=$status" +Write-Output "azd_environment=$azdEnvironment" +Write-Output "detected_existing_subscription_id=$existingSubscriptionId" +Write-Output "detected_existing_location=$existingLocation" +Write-Output "detected_default_subscription_id=$defaultSubscriptionId" +Write-Output "detected_default_location=$defaultLocation" +Write-Output "detected_az_subscription_name=$azSubscriptionName" +Write-Output "detected_az_subscription_id=$azSubscriptionId" +Write-Output "requested_subscription_id=$SubscriptionId" +Write-Output "requested_location=$Location" +Write-Output "verified_subscription_id=$verifiedSubscriptionId" +Write-Output "verified_location=$verifiedLocation" + +Write-Output '' +Write-Output 'AZD context summary:' +Write-Output " Environment: $(if ($azdEnvironment) { $azdEnvironment } else { '' })" +Write-Output " Existing azd values: subscription=$(if ($existingSubscriptionId) { $existingSubscriptionId } else { '' }), location=$(if ($existingLocation) { $existingLocation } else { '' })" +Write-Output " Defaults: subscription=$(if ($defaultSubscriptionId) { $defaultSubscriptionId } else { '' }), location=$(if ($defaultLocation) { $defaultLocation } else { '' })" +Write-Output " Azure CLI current: $(if ($azSubscriptionName) { $azSubscriptionName } else { '' }) ($(if ($azSubscriptionId) { $azSubscriptionId } else { '' }))" +if ($DetectOnly) { + Write-Output ' Action: detection only; no azd values changed.' +} else { + Write-Output " Applied and verified: subscription=$verifiedSubscriptionId, location=$verifiedLocation" +} diff --git a/plugin/skills/azure-prepare/references/scripts/set-azd-context.sh b/plugin/skills/azure-prepare/references/scripts/set-azd-context.sh new file mode 100644 index 000000000..a357f8be7 --- /dev/null +++ b/plugin/skills/azure-prepare/references/scripts/set-azd-context.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# Detect, apply, and verify azd subscription/location context. +# +# USAGE: +# ./set-azd-context.sh [environment-name] +# ./set-azd-context.sh --detect-only [environment-name] +# +# OUTPUT: +# Machine-readable key=value lines followed by a human-readable summary. + +set -euo pipefail + +DETECT_ONLY="false" +if [ "${1:-}" = "--detect-only" ]; then + DETECT_ONLY="true" + SUBSCRIPTION_ID="" + LOCATION="" + ENVIRONMENT_NAME="${2:-}" +else + SUBSCRIPTION_ID="${1:-}" + LOCATION="${2:-}" + ENVIRONMENT_NAME="${3:-}" + if [ -z "$SUBSCRIPTION_ID" ] || [ -z "$LOCATION" ]; then + echo "ERROR: Usage: $0 [environment-name]" >&2 + echo " or: $0 --detect-only [environment-name]" >&2 + exit 1 + fi +fi + +strip_quotes() { + value="${1%$'\r'}" + case "$value" in + \"*\") value=${value#\"}; value=${value%\"} ;; + \'*\') value=${value#\'}; value=${value%\'} ;; + esac + printf '%s' "$value" +} + +json_prop() { + prop="$1" + sed -n "s/.*\"$prop\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" | head -1 +} + +load_azd_values() { + AZD_SUBSCRIPTION_ID="" + AZD_LOCATION="" + if values=$(azd env get-values 2>/dev/null); then + while IFS= read -r line; do + [ -n "$line" ] || continue + key=${line%%=*} + value=$(strip_quotes "${line#*=}") + case "$key" in + AZURE_SUBSCRIPTION_ID) AZD_SUBSCRIPTION_ID="$value" ;; + AZURE_LOCATION) AZD_LOCATION="$value" ;; + esac + done </dev/null +fi + +AZD_ENV_LIST=$(azd env list 2>/dev/null || true) +if [ -z "$AZD_ENVIRONMENT" ]; then + AZD_ENVIRONMENT=$(printf '%s\n' "$AZD_ENV_LIST" | awk '/^\*/ { print $2; exit }') +fi + +load_azd_values +EXISTING_SUBSCRIPTION_ID="$AZD_SUBSCRIPTION_ID" +EXISTING_LOCATION="$AZD_LOCATION" + +DEFAULT_SUBSCRIPTION_ID="" +DEFAULT_LOCATION="" +if defaults_json=$(azd config get defaults 2>/dev/null); then + DEFAULT_SUBSCRIPTION_ID=$(printf '%s' "$defaults_json" | json_prop subscription) + DEFAULT_LOCATION=$(printf '%s' "$defaults_json" | json_prop location) +fi + +AZ_SUBSCRIPTION_NAME="" +AZ_SUBSCRIPTION_ID="" +if account_json=$(az account show --query "{name:name, id:id}" -o json 2>/dev/null); then + AZ_SUBSCRIPTION_NAME=$(printf '%s' "$account_json" | json_prop name) + AZ_SUBSCRIPTION_ID=$(printf '%s' "$account_json" | json_prop id) +fi + +if [ "$DETECT_ONLY" = "false" ]; then + azd env set AZURE_SUBSCRIPTION_ID "$SUBSCRIPTION_ID" >/dev/null + azd env set AZURE_LOCATION "$LOCATION" >/dev/null + + load_azd_values + if [ "$AZD_SUBSCRIPTION_ID" != "$SUBSCRIPTION_ID" ] || [ "$AZD_LOCATION" != "$LOCATION" ]; then + echo "status=failed" + echo "requested_subscription_id=$SUBSCRIPTION_ID" + echo "requested_location=$LOCATION" + echo "verified_subscription_id=$AZD_SUBSCRIPTION_ID" + echo "verified_location=$AZD_LOCATION" + echo "ERROR: azd context verification failed." >&2 + exit 1 + fi +fi + +STATUS="success" +[ "$DETECT_ONLY" = "true" ] && STATUS="detected" + +echo "status=$STATUS" +echo "azd_environment=$AZD_ENVIRONMENT" +echo "detected_existing_subscription_id=$EXISTING_SUBSCRIPTION_ID" +echo "detected_existing_location=$EXISTING_LOCATION" +echo "detected_default_subscription_id=$DEFAULT_SUBSCRIPTION_ID" +echo "detected_default_location=$DEFAULT_LOCATION" +echo "detected_az_subscription_name=$AZ_SUBSCRIPTION_NAME" +echo "detected_az_subscription_id=$AZ_SUBSCRIPTION_ID" +echo "requested_subscription_id=$SUBSCRIPTION_ID" +echo "requested_location=$LOCATION" +echo "verified_subscription_id=$AZD_SUBSCRIPTION_ID" +echo "verified_location=$AZD_LOCATION" + +echo "" +echo "AZD context summary:" +echo " Environment: ${AZD_ENVIRONMENT:-}" +echo " Existing azd values: subscription=${EXISTING_SUBSCRIPTION_ID:-}, location=${EXISTING_LOCATION:-}" +echo " Defaults: subscription=${DEFAULT_SUBSCRIPTION_ID:-}, location=${DEFAULT_LOCATION:-}" +echo " Azure CLI current: ${AZ_SUBSCRIPTION_NAME:-} (${AZ_SUBSCRIPTION_ID:-})" +if [ "$DETECT_ONLY" = "true" ]; then + echo " Action: detection only; no azd values changed." +else + echo " Applied and verified: subscription=$AZD_SUBSCRIPTION_ID, location=$AZD_LOCATION" +fi From 706529ca266b96090b9ff6e490c72908f7761c6a Mon Sep 17 00:00:00 2001 From: "Tom Meschter (from Dev Box)" Date: Mon, 8 Jun 2026 09:39:24 -0700 Subject: [PATCH 2/2] refactor: keep Aspire troubleshooting inline in aspire.md Restore the troubleshooting guidance that was extracted into a separate aspire-troubleshooting.md file back into aspire.md, and delete the separate file. The script-based azd context changes are unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../references/aspire-troubleshooting.md | 59 ------- .../skills/azure-prepare/references/aspire.md | 144 +++++++++++++++++- 2 files changed, 141 insertions(+), 62 deletions(-) delete mode 100644 plugin/skills/azure-prepare/references/aspire-troubleshooting.md diff --git a/plugin/skills/azure-prepare/references/aspire-troubleshooting.md b/plugin/skills/azure-prepare/references/aspire-troubleshooting.md deleted file mode 100644 index 6f1f4372f..000000000 --- a/plugin/skills/azure-prepare/references/aspire-troubleshooting.md +++ /dev/null @@ -1,59 +0,0 @@ -# Aspire Troubleshooting - -## Non-interactive `azd init` prompts - -- `no default response for prompt 'Enter a unique environment name:'` → include `-e `. -- `no default response for prompt 'How do you want to initialize your app?'` → include `--from-code`. - -```bash -ENV_NAME="$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | tr ' _' '-')-dev" -azd init --from-code -e "$ENV_NAME" -``` - -## No AppHost detected - -1. Verify AppHost project exists: `find . -name "*.AppHost.csproj"` -2. Check project builds: `dotnet build` -3. Ensure Aspire.Hosting is referenced by the AppHost project. - -## Unsupported resource type - -If manifest generation fails with `unsupported resource type`, the AppHost contains custom Aspire resources that azd cannot deploy. - -1. Do **not** modify source code to add `.ExcludeFromManifest()` or otherwise suppress the error. -2. Do **not** proceed with deployment. -3. Record a blocker: "AppHost contains custom Aspire resource types not supported for Azure deployment." -4. Tell the user the application targets local development or custom tooling, not Azure deployment. - -## Azure Functions secret storage - -When Aspire Functions use `.WithHostStorage(storage)`, Azure Functions secret/key management cannot use identity-based storage URIs. Before `azd up`, add file-based secret storage: - -```csharp -var functions = builder.AddAzureFunctionsProject("functions") - .WithHostStorage(storage) - .WithEnvironment("AzureWebJobsSecretStorageType", "Files"); -``` - -If generated infrastructure must be edited directly, ensure the Functions container app has: - -```bicep -{ - name: 'AzureWebJobsSecretStorageType' - value: 'Files' -} -``` - -## Wrong subscription - -The Azure CLI and azd keep separate contexts. After `azd init --from-code`, immediately run: - -```bash -./scripts/set-azd-context.sh -``` - -Use the PowerShell helper on Windows: - -```powershell -.\scripts\set-azd-context.ps1 -SubscriptionId -Location -EnvironmentName -``` diff --git a/plugin/skills/azure-prepare/references/aspire.md b/plugin/skills/azure-prepare/references/aspire.md index bc663b229..25cbf4c9c 100644 --- a/plugin/skills/azure-prepare/references/aspire.md +++ b/plugin/skills/azure-prepare/references/aspire.md @@ -63,7 +63,7 @@ ENV_NAME="$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | tr ' _' '-')-dev" azd init --from-code -e "$ENV_NAME" ``` -**If `azd init --from-code` fails with `unsupported resource type`:** do not patch AppHost source or proceed. Record a blocker that custom Aspire resource types cannot be deployed to Azure. See [Aspire troubleshooting](aspire-troubleshooting.md). +**If `azd init --from-code` fails with `unsupported resource type`:** do not patch AppHost source or proceed. Record a blocker that custom Aspire resource types cannot be deployed to Azure. See [Troubleshooting](#troubleshooting). ### Step 3: Configure Subscription and Location @@ -132,7 +132,7 @@ var functions = builder.AddAzureFunctionsProject("function .WithReference(queues); ``` -See [aspire-functions-secrets reference](services/functions/aspire-containerapps.md) and [Aspire troubleshooting](aspire-troubleshooting.md) for details. +See [aspire-functions-secrets reference](services/functions/aspire-containerapps.md) and [Troubleshooting](#troubleshooting) for details. ## Flags Reference @@ -156,7 +156,145 @@ azd init --from-code -e "$ENV_NAME" ## Troubleshooting -See [Aspire troubleshooting](aspire-troubleshooting.md) for non-interactive `azd init`, missing AppHost, unsupported resource type, Azure Functions secret storage, and wrong-subscription fixes. +### Error: "no default response for prompt 'Enter a unique environment name:'" + +**Cause:** Missing `-e` flag when running `azd init --from-code` in non-interactive environment +**Solution:** Always include the `-e ` flag + +```bash +# ❌ Wrong - fails in non-interactive environments (agents, CI/CD) +azd init --from-code + +# ✅ Correct - provides environment name upfront +ENV_NAME="$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | tr ' _' '-')-dev" +azd init --from-code -e "$ENV_NAME" +``` + +**Important:** This error typically occurs when: +- Running in an agent or automation context +- No TTY is available for interactive prompts +- The `-e` flag was omitted + +### Error: "no default response for prompt 'How do you want to initialize your app?'" + +**Cause:** Missing `--from-code` flag +**Solution:** Add `--from-code` to the `azd init` command + +```bash +# ❌ Wrong - requires interactive prompt +azd init -e "my-env" + +# ✅ Correct - auto-detects AppHost +azd init --from-code -e "my-env" +``` + +### No AppHost detected + +**Symptoms:** `azd init --from-code` doesn't find the AppHost + +**Solutions:** +1. Verify AppHost project exists: `find . -name "*.AppHost.csproj"` +2. Check project builds: `dotnet build` +3. Ensure Aspire.Hosting package is referenced in AppHost project + +### Error: "unsupported resource type" during manifest generation + +**Symptoms:** `azd init --from-code` fails with output like: +``` +error: unsupported resource type: +``` +or the manifest generation step errors on child resources (e.g., ClockHand, or other custom resource types defined in the AppHost). + +**Cause:** The AppHost contains custom Aspire resource types that azd cannot convert to Azure deployable resources. These custom types are typically: +- Demonstration resources showing developers how to build Aspire extensions for local tooling +- Resources that wrap local services without Azure equivalents +- Custom child resources (e.g., subcomponents of a custom Aspire integration) + +**Resolution:** + +1. ⛔ **Do NOT attempt to fix this error by modifying source code** — do not add `.ExcludeFromManifest()` calls or otherwise patch the AppHost +2. ⛔ **Do NOT proceed with deployment** — this is a deployment blocker, not a recoverable error +3. ✅ Record a blocker in the deployment plan: "AppHost contains custom Aspire resource types not supported for Azure deployment (unsupported resource type)" +4. ✅ Inform the user that this application is designed for local development and cannot be meaningfully deployed to Azure + +> ⚠️ **Why this is a hard stop:** Custom resource types that produce "unsupported resource type" errors are intentionally not deployable. Adding `.ExcludeFromManifest()` to suppress the error may allow `azd init` to succeed, but the resulting deployment would not represent the application's actual functionality. + +### Azure Functions: Secret initialization from Blob storage failed + +**Symptoms:** Azure Functions app fails at startup with error: +``` +System.InvalidOperationException: Secret initialization from Blob storage failed due to missing both +an Azure Storage connection string and a SAS connection uri. +``` + +**Cause:** When using `AddAzureFunctionsProject` with `WithHostStorage(storage)`, Aspire configures identity-based storage access (managed identity). However, Azure Functions' internal secret management does not support identity-based URIs and requires file-based secret storage for Container Apps deployments. + +**Solution:** Add `AzureWebJobsSecretStorageType=Files` environment variable to the Functions resource in the AppHost **before running `azd up`**: + +```csharp +var functions = builder.AddAzureFunctionsProject("functions") + .WithReference(queues) + .WithReference(blobs) + .WaitFor(storage) + .WithRoleAssignments(storage, ...) + .WithHostStorage(storage) + .WithEnvironment("AzureWebJobsSecretStorageType", "Files") // Required for Container Apps + .WithUrlForEndpoint("http", u => u.DisplayText = "Functions App"); +``` + +> 💡 **Why this is required:** +> - `WithHostStorage(storage)` sets identity-based URIs like `AzureWebJobsStorage__blobServiceUri` +> - This is correct and secure for runtime storage operations +> - However, Functions' secret/key management doesn't support these URIs +> - File-based secrets are mandatory for Container Apps deployments + +> ⚠️ **Important:** This is required when: +> - Using `AddAzureFunctionsProject` in Aspire +> - Using `WithHostStorage()` with identity-based storage +> - Deploying to Azure Container Apps (the default for Aspire Functions) + +**Generated Infrastructure Note:** + +If you need to modify the generated Container Apps infrastructure directly, ensure the Functions container app has this environment variable: + +```bicep +resource functionsContainerApp 'Microsoft.App/containerApps@2024-03-01' = { + properties: { + template: { + containers: [ + { + env: [ + { + name: 'AzureWebJobsSecretStorageType' + value: 'Files' + } + // ... other environment variables + ] + } + ] + } + } +} +``` + +### Error: azd uses wrong subscription despite user confirmation + +**Symptoms:** `azd provision --preview` shows a different subscription than the one the user confirmed + +**Cause:** The `AZURE_SUBSCRIPTION_ID` was not set immediately after `azd init --from-code`. The Azure CLI and azd can have different default subscriptions. + +**Solution:** Always set the subscription immediately after initialization: + +```bash +# After azd init --from-code completes: +azd env set AZURE_SUBSCRIPTION_ID +azd env set AZURE_LOCATION + +# Verify before proceeding: +azd env get-values +``` + +**Prevention:** Follow the complete initialization sequence in the [Flags Reference](#azd-init-for-aspire) section above. ## References