SKILL DETAIL
azure-validate
microsoft/azure-skills/azure-validate
The azure-validate skill performs comprehensive pre-flight checks on Azure resources before deployment. It validates configuration, infrastructure (Bicep or Terraform), RBAC role assignments, managed identity permissions, and prerequisites to ensure the application meets deployment requirements. This skill is useful for validating azure.yaml, Bicep files, Azure Functions, Container Apps, and troubleshooting deployment errors. To use this skill, first run azure-prepare to generate a deployment plan, then follow the workflow script step-by-step to complete validation. Only after all checks pass can the status be set to Validated. After validation, decide whether to invoke azure-deploy based on user intent.
Installation
npx skills add https://github.com/microsoft/azure-skills --skill azure-validate
技能檔案
SKILL.md
最近同步 · 2026年8月29日
references/aspire-functions-secrets.md›
# Aspire + Azure Functions: Secret Storage Validation
> ⚠️ **Pre-provisioning check** — Run this BEFORE `azd provision`.
## When This Applies
This check is required when **all** of these are true:
| Condition | How to detect |
|-----------|--------------|
| .NET Aspire project | `*.AppHost.csproj` exists or `Aspire.Hosting` package reference |
| Azure Functions component | `AddAzureFunctionsProject` call in `AppHost.cs` or `Program.cs` |
| Identity-based storage | `WithHostStorage` call (Aspire default) |
## Detection
Run the scan script — it finds `*.cs` files that call `AddAzureFunctionsProject`, checks whether each one already sets `AzureWebJobsSecretStorageType`, and prints a single verdict so you don't have to parse raw grep output.
**Bash** — [`scripts/scan-aspire-functions-secrets.sh`](scripts/scan-aspire-functions-secrets.sh):
```bash
./scripts/scan-aspire-functions-secrets.sh [directory] # directory defaults to .
```
**PowerShell** — [`scripts/scan-aspire-functions-secrets.ps1`](scripts/scan-aspire-functions-secrets.ps1):
```powershell
.\scripts\scan-aspire-functions-secrets.ps1 [-Path <directory>] # -Path defaults to .
```
The script prints one of three verdicts:
| Verdict | Meaning | Action |
|---------|---------|--------|
| `NOT APPLICABLE` | No `AddAzureFunctionsProject` call found | Skip this check |
| `ALREADY CONFIGURED` | Every matching file already sets `AzureWebJobsSecretStorageType` | No change required |
| `FIX REQUIRED` | Matching file(s) call `AddAzureFunctionsProject` but omit the setting (file/line listed) | Apply the **Fix** below to each listed file |
## Fix
Add `.WithEnvironment("AzureWebJobsSecretStorageType", "Files")` to the Azure Functions project builder chain in the AppHost source file that contains the `AddAzureFunctionsProject` call (often `Program.cs` in the `*.AppHost` project).
### Before
```csharp
var functions = builder.AddAzureFunctionsProject<Projects.MyFunctions>("functions")
.WithHostStorage(storage)
.WithReference(queues);
```
### After
```csharp
var functions = builder.AddAzureFunctionsProject<Projects.MyFunctions>("functions")
.WithHostStorage(storage)
.WithEnvironment("AzureWebJobsSecretStorageType", "Files")
.WithReference(queues);
```
> 💡 **Tip:** Place `.WithEnvironment(...)` immediately after `.WithHostStorage(...)` for clarity.
## Why This Is Required
Azure Functions needs storage for managing host secrets/keys (function keys, host keys, master key). By default, it stores them as blobs in the `AzureWebJobsStorage` account.
When Aspire configures identity-based storage access (via `WithHostStorage`), it sets URI-based environment variables like `AzureWebJobsStorage__blobServiceUri` instead of a connection string. The Functions runtime's secret manager does **not** support these identity-based URIs — it requires either a connection string or SAS token.
Setting `AzureWebJobsSecretStorageType=Files` switches the Functions host to file-system-based key storage, bypassing the blob storage dependency for secrets.
## Error Without This Setting
```
System.InvalidOperationException: Secret initialization from Blob storage failed
due to missing both an Azure Storage connection string and a SAS connection URI.
For Blob Storage, please provide at least one of these.
```
## When This Check Does NOT Apply
| Scenario | Why |
|----------|-----|
| Aspire project without Azure Functions | No Functions secret manager involved |
| Standalone Azure Functions (not Aspire) | Uses connection string by default |
| Functions with explicit connection string | `AzureWebJobsStorage` is a full connection string, not identity-based |
| `AzureWebJobsSecretStorageType` already set | Configuration is already present |
references/global-rules.md›
# Global Rules
> **MANDATORY** — These rules apply to ALL skills. Violations are unacceptable.
## Rule 1: Destructive Actions Require User Confirmation
⛔ **ALWAYS use `ask_user`** before ANY destructive action.
### What is Destructive?
| Category | Examples |
|----------|----------|
| **Delete** | `az group delete`, `azd down`, `rm -rf`, delete resource |
| **Overwrite** | Replace existing files, overwrite config, reset settings |
| **Irreversible** | Purge Key Vault, delete storage account, drop database |
| **Cost Impact** | Provision expensive resources, scale up significantly |
| **Security** | Expose secrets, change access policies, modify RBAC |
### How to Confirm
```
ask_user(
question: "This will permanently delete resource group 'rg-myapp'. Continue?",
choices: ["Yes, delete it", "No, cancel"]
)
```
### No Exceptions
- Do NOT assume user wants to delete/overwrite
- Do NOT proceed based on "the user asked to deploy" (deploy ≠ delete old)
- Do NOT batch destructive actions without individual confirmation
---
## Rule 2: Never Assume Subscription or Location
⛔ **ALWAYS use `ask_user`** to confirm:
- Azure subscription (show actual name and ID)
- Azure region/location
references/policy-validation.md›
# Azure Policy Validation
## How to Validate Policies
### 1. Get Subscription ID
Retrieve your current Azure subscription ID:
```bash
az account show --query id -o tsv
```
### 2. Validate Policies
Call the Azure MCP Policy tool to retrieve policies for your subscription:
```
mcp_azure_mcp_policy(command: "list", parameters: { subscription_id: "<subscription-id>" })
```
Replace `<subscription-id>` with the actual subscription ID obtained from step 1.
## Review Policy Compliance
When validating Azure policies for your subscription:
- **Check for policy violations** — Identify any resources or configurations that don't comply with assigned policies
- **Verify organizational compliance** — Ensure the planned deployment meets all organizational policy requirements
- **Address policy conflicts** — Resolve any policy issues before proceeding to deployment
## Common Policy Issues
| Issue | Resolution |
|-------|------------|
| Non-compliant resource SKUs | Update resource SKUs to comply with allowed values |
| Missing required tags | Add required tags to resources in your infrastructure code |
| Disallowed resource types | Replace with allowed alternatives or request policy exception |
| Location restrictions | Deploy to allowed regions only |
| Network security violations | Update NSG rules, firewall settings, or virtual network configurations |
## Next Steps
Only proceed to deployment after all policy violations are resolved and compliance is confirmed.
references/recipes/azcli/errors.md›
# AZCLI Validation Errors
| Error | Fix |
|-------|-----|
| `AADSTS700082: Token expired` | `az login` |
| `Please run 'az login'` | `az login` |
| `AADSTS50076: MFA required` | `az login --use-device-code` |
| `AuthorizationFailed` | Request Contributor role |
| `npm ci` fails with `missing: package-lock.json` | Run `npm install --package-lock-only` in the service directory before building |
| `Template validation failed` | Check Bicep syntax |
## Debug
```bash
az <command> --verbose --debug
```
references/recipes/azcli/README.md›
# AZCLI Validation
Validation steps for Azure CLI deployments.
## Prerequisites
- `./infra/main.bicep` exists
- Docker available (if containerized)
## Validation Steps
- [ ] 1. Core Validation (CLI, auth, build, validate, what-if) — run [`validate-deployment` script](../scripts/validate-deployment.sh)
- [ ] 2. Docker Build (if containerized)
- [ ] 3. Azure Policy Validation
## Validation Details
### 1. Core Validation Script
The core validation checks are a fixed, deterministic sequence. Run the shared
**validate-deployment** helper instead of executing and parsing each command by hand. It
confirms the Azure CLI is installed and authenticated, compiles the Bicep template
(`az bicep build`), validates it against the target scope (`az deployment ... validate`),
and runs a what-if preview — printing a compact PASS/FAIL summary plus a what-if change
count (Create/Modify/Delete).
- Bash: [`../scripts/validate-deployment.sh`](../scripts/validate-deployment.sh)
- PowerShell: [`../scripts/validate-deployment.ps1`](../scripts/validate-deployment.ps1)
**Subscription scope:**
```bash
../scripts/validate-deployment.sh --scope sub --location <location>
```
```powershell
../scripts/validate-deployment.ps1 -Scope sub -Location <location>
```
**Resource group scope:**
```bash
../scripts/validate-deployment.sh --scope group --resource-group <rg-name>
```
```powershell
../scripts/validate-deployment.ps1 -Scope group -ResourceGroup <rg-name>
```
Defaults: `--template ./infra/main.bicep`, `--parameters ./infra/main.parameters.json`
(skipped if absent). Pass `--subscription <id>` to target a specific subscription.
**Interpreting results:**
- `OVERALL: PASS` — all five checks passed; record the summary in Section 7 (Validation Proof).
- Any step `FAIL` — the script prints the failing command's error. Remediate:
- **Authenticated** fails → `az login`, then `az account set --subscription <id>`.
- **Azure CLI installed** fails → install via `mcp_azure_mcp_extension_cli_install(cli-type: "az")`.
- Otherwise see [Error handling](./errors.md).
### 2. Docker Build (if containerized)
**Before building**, validate the Docker build context:
1. Read the `Dockerfile` in `./src/<service>`
2. If the Dockerfile contains `npm ci`, verify `package-lock.json` exists in the same directory
3. If `package-lock.json` is missing, generate it:
```bash
cd ./src/<service>
npm install --package-lock-only
```
**Then build:**
```bash
docker build -t <image>:test ./src/<service>
```
### 3. Azure Policy Validation
See [Policy Validation Guide](../../policy-validation.md) for instructions on retrieving and validating Azure policies for your subscription.
## References
- [Error handling](./errors.md)
## Next
All checks pass → **azure-deploy**
references/recipes/azd/aspire.md›
# Aspire Validation
> ⚠️ **Only load this file when the project is a .NET Aspire application.**
Validation steps specific to .NET Aspire projects deployed via AZD.
## Detection
A project is Aspire-based if any of these are true:
| Indicator | Check |
|-----------|-------|
| AppHost project | `find . -name "*.AppHost.csproj"` |
| Aspire.Hosting package | `grep -r "Aspire.Hosting" . --include="*.csproj"` |
**If none found → skip this file entirely.**
---
## Pre-Provisioning: Functions Secret Storage
> ⚠️ **CRITICAL — Must run BEFORE `azd provision`.**
Check if the project uses Azure Functions within Aspire and ensure `AzureWebJobsSecretStorageType` is configured.
See [Aspire Functions Secrets Reference](../../aspire-functions-secrets.md) for detection commands, fix examples, and full details.
**If `AddAzureFunctionsProject` is NOT found**, skip this section.
---
## Post-Provisioning: Container Apps Environment Variables
> ⚠️ **CRITICAL — Run AFTER `azd provision` but BEFORE `azd deploy`.**
When using Aspire with Container Apps in "limited mode" (in-memory infrastructure generation), `azd provision` creates Azure resources but doesn't automatically populate environment variables that `azd deploy` needs.
**Run the helper script** ([scripts/set-aspire-aca-env.sh](scripts/set-aspire-aca-env.sh) or [scripts/set-aspire-aca-env.ps1](scripts/set-aspire-aca-env.ps1)). It sets `AZURE_CONTAINER_REGISTRY_ENDPOINT`, `AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID`, and `MANAGED_IDENTITY_CLIENT_ID` (only the ones that are missing).
**bash:**
```bash
./scripts/set-aspire-aca-env.sh # or: -e <azd-env-name>
```
**PowerShell:**
```powershell
./scripts/set-aspire-aca-env.ps1 # or: -Environment <azd-env-name>
```
references/recipes/azd/environment.md›
# AZD Environment Setup
> **⛔ MANDATORY**: You MUST set up an AZD environment before running any deployment commands.
## Step 1: Check Existing Environments
```bash
azd env list
```
**If an environment is already selected** (marked with `*`), check its current values:
```bash
azd env get-values
```
If `AZURE_ENV_NAME`, `AZURE_SUBSCRIPTION_ID`, and `AZURE_LOCATION` are already set, confirm with user:
```
Question: "An AZD environment is already configured. Would you like to use it?"
Environment: {env-name}
Subscription: {subscription-id}
Location: {location}
Choices: [
"Yes, use this environment (Recommended)",
"No, create a new environment"
]
```
If user confirms → skip to **Verify Configuration**. Otherwise → continue to Step 2.
**If NO environment exists or none is selected:**
- Continue to Step 2
---
## Step 2: Create New Environment
> **⛔ DO NOT use generic names like "dev", "prod", or "test"**
>
> These cause naming conflicts in Azure resource groups and resources. Always generate a unique name.
### Generate Suggested Name
Use this pattern:
```
{project-name}-{random-4-chars}
```
**Examples:**
- `dadjokes-x7k2`
- `todoapp-m3p9`
- `myapi-q5w8`
### Prompt User
**You MUST use `ask_user` to confirm the environment name:**
```
I need to create an AZD environment for this deployment.
Suggested name: {project-name}-{random-4-chars}
Would you like to use this name or enter a custom one?
```
### Create Environment
After user confirms:
```bash
azd env new <environment-name> --no-prompt
```
---
## Step 3: Configure Environment
Set subscription and location:
```bash
azd env set AZURE_SUBSCRIPTION_ID <subscription-id>
azd env set AZURE_LOCATION <location>
```
---
## Verify Configuration
```bash
azd env get-values
```
Confirm these values are set:
- `AZURE_ENV_NAME`
- `AZURE_SUBSCRIPTION_ID`
- `AZURE_LOCATION`
---
## Only Then Proceed
After environment is configured, proceed with `azd up --no-prompt`.
references/recipes/azd/errors.md›
# AZD Validation Errors
## Pre-Deployment Errors
These errors can be caught **before** running `azd up`:
| Error | Cause | Resolution |
|-------|-------|------------|
| `Please run 'az login'` | Not authenticated | `az login` or `azd auth login` |
| `No environment selected` | Missing azd environment | `azd env select <name>` or `azd env new <name> --no-prompt` |
| `no default response for prompt 'Enter a unique environment name'` | No azd environment created, or missing `-e` flag | Run `azd env new <name> --no-prompt` OR use `azd init --from-code -e <name> --no-prompt` with the `-e` flag |
| `no default response for prompt 'Enter a value for the 'environmentName'` | Environment variables not set | Run `azd env set AZURE_ENV_NAME <name>` |
| `Service not found` | Service name mismatch | Check service name in azure.yaml |
| `Invalid azure.yaml` | YAML syntax error | Fix YAML syntax |
| `Project path does not exist` | Wrong service project path | Fix service project path in azure.yaml |
| `Cannot connect to Docker daemon` | Docker not running | Start Docker Desktop |
| `npm ci` fails with `missing: package-lock.json` | Dockerfile uses `npm ci` but `package-lock.json` not in build context | Run `npm install --package-lock-only` in the service directory before building |
| `Could not find a part of the path 'infra\main.bicep'` | Missing infrastructure files | Generate infra/ folder before `azd up` |
| `Invalid resource group location '<loc>'. The Resource group already exists in location '<other>'` | RG exists in different region | Check RG location first with `az group show`, use that region or new env name |
| `expecting only '1' resource tagged with 'azd-service-name: web', but found '2'` | Multiple resources with same tag **in the same RG** | Delete duplicate or rename service |
## Static Web App Errors
| Error | Cause | Fix |
|-------|-------|-----|
| `language 'html' is not supported` | Invalid language value | Omit `language` for pure static sites |
| `language 'static' is not supported` | Invalid language value | Omit `language` for pure static sites |
| `dist folder not found` | Wrong dist path or missing build | Check `dist` is relative to `project`; add `language: js` if build needed |
| `LocationNotAvailableForResourceType` | SWA not in region | See [Region Availability](../../region-availability.md) for valid regions |
## SWA Path Validation
Before deployment, verify:
1. `project` path exists and contains source files
2. For framework apps: `language: js` is set
3. `dist` is relative to `project` (not project root)
4. Bicep has `azd-service-name` tag matching service name
## Debug
```bash
azd <command> --debug
```
references/recipes/azd/README.md›
# AZD Validation
Validation steps for Azure Developer CLI projects.
## Prerequisites
- `azure.yaml` exists in project root
- Infrastructure files exist:
- For Bicep: `./infra/` contains Bicep files
- For Terraform: `./infra/` contains `.tf` files and `azure.yaml` has `infra.provider: terraform`
## Validation Steps
- [ ] 1. AZD Installation
- [ ] 2. Schema Validation
- [ ] 3. Environment Setup
- [ ] 4. Authentication Check
- [ ] 5. Subscription/Location Check
- [ ] 6. Aspire Pre-Provisioning Checks
- [ ] 7. Provision Preview
- [ ] 8. Build Verification
- [ ] 9. Docker Build Context Validation
- [ ] 10. Package Validation
- [ ] 11. Azure Policy Validation
- [ ] 12. Aspire Post-Provisioning Checks
## Validation Details
### 1. AZD Installation
Verify AZD is installed:
```bash
azd version
```
**If not installed:**
```
mcp_azure_mcp_extension_cli_install(cli-type: "azd")
```
### 2. Schema Validation
Validate azure.yaml against official schema:
```
mcp_azure_mcp_azd(command: "validate_azure_yaml", parameters: { path: "./azure.yaml" })
```
### 3. Environment Setup
Verify AZD environment exists and is configured. See [Environment Setup](environment.md) for detailed steps.
### 4. Authentication Check
```bash
azd auth login --check-status
```
**If not logged in:**
```bash
azd auth login
```
### 5. Subscription/Location Check
Check environment values:
```bash
azd env get-values
```
**If AZURE_SUBSCRIPTION_ID or AZURE_LOCATION not set:**
Use Azure MCP tools to list subscriptions:
```
mcp_azure_mcp_subscription_list
```
Use Azure MCP tools to list resource groups (check for conflicts):
```
mcp_azure_mcp_group_list
subscription: <subscription-id>
```
Prompt user to confirm subscription and location before continuing.
Refer to the region availability reference to select a region supported by all services in this template:
- [Region availability](../../region-availability.md)
```bash
azd env set AZURE_SUBSCRIPTION_ID <subscription-id>
azd env set AZURE_LOCATION <location>
```
### 6. Aspire Pre-Provisioning Checks
**If this is a .NET Aspire project** (detected by `*.AppHost.csproj` or `Aspire.Hosting` package reference), run the **Pre-Provisioning** checks in [Aspire Validation](aspire.md) before continuing. **If not Aspire, skip this step.**
### 7. Provision Preview
Validate IaC is ready (must complete without error):
```bash
azd provision --preview --no-prompt
```
> 💡 **Note:** This works for both Bicep and Terraform. azd will automatically detect the provider from `azure.yaml` and run the appropriate validation (`bicep build` or `terraform plan`).
**If `azd provision --preview` fails with `unsupported resource type`:**
⛔ **Stop deployment immediately.** Do NOT attempt to fix or work around the error.
This error means the Aspire AppHost contains custom resource types that have no Azure deployment target (e.g., `HealthChecksUI`, custom child resources, or local-only integrations). These resources are intentionally designed for local development tooling and cannot be meaningfully deployed to Azure.
**Required actions:**
1. ⛔ **Do NOT modify source code** to suppress the error (e.g., do not add `.ExcludeFromManifest()`).
2. ⛔ **Do NOT proceed with `azd provision` or `azd deploy`.**
3. ✅ Inform the user: "This application contains custom Aspire resource types (`unsupported resource type: <type-name>`) that cannot be deployed to Azure. The application is designed for local development only."
4. ✅ Record a blocker: "Deployment blocked — AppHost contains unsupported resource types with no Azure deployment target."
> ⚠️ Adding `.ExcludeFromManifest()` to suppress the error may allow provisioning to proceed, but the resulting deployment will not represent the application's actual functionality and is not a valid deployment.
### 8. Build Verification
Build the project and verify there are no errors. If the build fails, fix the issues and re-build until it succeeds. Do NOT proceed to packaging or deployment with build errors.
### 9. Docker Build Context Validation
**If any service in `azure.yaml` uses a Dockerfile** (check the service's `project` path from `azure.yaml` for a `Dockerfile`), validate the build context before packaging:
1. Read each service's `Dockerfile`
2. If the Dockerfile contains `npm ci`, verify `package-lock.json` exists in the same directory
3. If `package-lock.json` is missing, generate it in the service's `project` path directory before proceeding:
```bash
cd <service-project-path>
npm install --package-lock-only
```
> ⚠️ **Warning:** `npm ci` will fail during Docker build if `package-lock.json` is missing. This check prevents Docker build failures during `azd package` and `azd up`.
### 10. Package Validation
Confirm all services package successfully:
```bash
azd package --no-prompt
```
### 11. Azure Policy Validation
See [Policy Validation Guide](../../policy-validation.md) for instructions on retrieving and validating Azure policies for your subscription.
### 12. Aspire Post-Provisioning Checks
**If this is a .NET Aspire project**, run the **Post-Provisioning** checks in [Aspire Validation](aspire.md) before proceeding to deployment. **If not Aspire, skip this step.**
## References
- [Environment Setup](environment.md)
- [Aspire Validation](aspire.md)
- [Error Handling](./errors.md)
## Next
All checks pass → **azure-deploy**
references/recipes/azd/scripts/set-aspire-aca-env.ps1›
<#
.SYNOPSIS
Set the Container Apps environment variables that Aspire "limited mode" leaves unpopulated.
.DESCRIPTION
When Aspire runs in "limited mode", `azd provision` creates the Azure resources
(Container Registry, Managed Identity, Container Apps Environment) but does NOT populate the
env vars that `azd deploy` needs to reference them. This script fills that gap.
Run it AFTER `azd provision` but BEFORE `azd deploy`.
The script only sets a variable if it is currently missing, and prints what it did so the
result can be understood without re-inspecting `azd env get-values`:
AZURE_CONTAINER_REGISTRY_ENDPOINT <- az acr list ... [0].loginServer
AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID <- az identity list ... [0].id
MANAGED_IDENTITY_CLIENT_ID <- az identity list ... [0].clientId
.PARAMETER Environment
Optional azd environment name (forwarded to `azd env` calls).
Defaults to the current/default azd environment.
.EXAMPLE
./set-aspire-aca-env.ps1
.EXAMPLE
./set-aspire-aca-env.ps1 -Environment my-azd-env
#>
param(
[string]$Environment
)
# Shared `-e <name>` argument list for azd calls (empty when no env name given).
$azdEnvArgs = @()
if ($Environment) {
$azdEnvArgs = @('-e', $Environment)
}
# Capture azd environment values and verify the call succeeded. PowerShell does not treat a
# native command's non-zero exit code as a terminating error, so check $LASTEXITCODE explicitly
# rather than relying on $ErrorActionPreference.
$azdOutput = azd env get-values @azdEnvArgs
if ($LASTEXITCODE -ne 0) {
Write-Error "'azd env get-values' failed with exit code $LASTEXITCODE. Run 'azd provision' before this script so the azd environment is available."
exit 1
}
# Load azd environment values into a hashtable.
$azdValues = @{}
foreach ($line in $azdOutput) {
if (-not $line) { continue }
$name, $value = $line.Split('=', 2)
$azdValues[$name] = $value.Trim('"')
}
$rgName = $azdValues['AZURE_RESOURCE_GROUP']
if (-not $rgName) {
Write-Error "AZURE_RESOURCE_GROUP is not set in the azd environment. Run 'azd provision' before this script so the resource group is available."
exit 1
}
function Set-IfMissing {
param(
[string]$VarName,
[string]$Description,
[scriptblock]$ValueCommand
)
$existing = $azdValues[$VarName]
if ($existing) {
Write-Host "${VarName}: already present ($existing)"
return
}
$value = (& $ValueCommand)
if (-not $value) {
Write-Error "Could not resolve $VarName ($Description) in resource group '$rgName'. Confirm 'azd provision' completed and the resource exists."
exit 1
}
azd env set @azdEnvArgs $VarName $value
Write-Host "${VarName}: set to $value"
}
Write-Host "Resource group: $rgName"
Set-IfMissing `
-VarName 'AZURE_CONTAINER_REGISTRY_ENDPOINT' `
-Description 'container registry login server' `
-ValueCommand { az acr list --resource-group $rgName --query "[0].loginServer" -o tsv }
Set-IfMissing `
-VarName 'AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID' `
-Description 'managed identity resource id' `
-ValueCommand { az identity list --resource-group $rgName --query "[0].id" -o tsv }
Set-IfMissing `
-VarName 'MANAGED_IDENTITY_CLIENT_ID' `
-Description 'managed identity client id' `
-ValueCommand { az identity list --resource-group $rgName --query "[0].clientId" -o tsv }
Write-Host "Aspire Container Apps environment variables are ready for 'azd deploy'."
references/recipes/azd/scripts/set-aspire-aca-env.sh›
#!/usr/bin/env bash
# Set the Container Apps environment variables that Aspire "limited mode" leaves unpopulated.
#
# When Aspire runs in "limited mode", `azd provision` creates the Azure resources
# (Container Registry, Managed Identity, Container Apps Environment) but does NOT populate the
# env vars that `azd deploy` needs to reference them. This script fills that gap.
#
# Run it AFTER `azd provision` but BEFORE `azd deploy`.
#
# USAGE:
# ./set-aspire-aca-env.sh [-e <azd-env-name>]
#
# -e, --environment Optional azd environment name (forwarded to `azd env` calls).
# Defaults to the current/default azd environment.
#
# The script only sets a variable if it is currently missing, and prints what it did so the
# result can be understood without re-inspecting `azd env get-values`:
# AZURE_CONTAINER_REGISTRY_ENDPOINT <- az acr list ... [0].loginServer
# AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID <- az identity list ... [0].id
# MANAGED_IDENTITY_CLIENT_ID <- az identity list ... [0].clientId
set -e
AZD_ENV_NAME=""
while [ $# -gt 0 ]; do
case "$1" in
-e|--environment)
AZD_ENV_NAME="$2"
shift 2
;;
*)
echo "ERROR: Unknown argument: $1" >&2
echo "USAGE: ./set-aspire-aca-env.sh [-e <azd-env-name>]" >&2
exit 1
;;
esac
done
# Build the shared `-e <name>` argument list for azd calls (empty when no env name given).
AZD_ENV_ARGS=""
if [ -n "$AZD_ENV_NAME" ]; then
AZD_ENV_ARGS="-e $AZD_ENV_NAME"
fi
# Capture azd environment values via command substitution so `set -e` aborts if the
# `azd env get-values` call itself fails (rather than silently continuing with no values).
AZD_VALUES=$(azd env get-values $AZD_ENV_ARGS)
# get_env_value <KEY> — print the (unquoted) value of KEY from AZD_VALUES, empty if absent.
# Uses only POSIX-friendly tools so it works on the widely-available Bash 3.2 (e.g. macOS).
get_env_value() {
printf '%s\n' "$AZD_VALUES" \
| grep "^$1=" \
| head -n 1 \
| sed -e "s/^$1=//" -e 's/^"\(.*\)"$/\1/' -e "s/^'\(.*\)'$/\1/"
}
RG_NAME=$(get_env_value AZURE_RESOURCE_GROUP)
if [ -z "$RG_NAME" ]; then
echo "ERROR: AZURE_RESOURCE_GROUP is not set in the azd environment." >&2
echo "Run 'azd provision' before this script so the resource group is available." >&2
exit 1
fi
# set_if_missing <ENV_VAR_NAME> <description> <resolver command...>
# The resolver command is passed as arguments and run via "$@" — no eval.
set_if_missing() {
var_name="$1"
description="$2"
shift 2
existing=$(get_env_value "$var_name")
if [ -n "$existing" ]; then
echo "$var_name: already present ($existing)"
return 0
fi
value=$("$@")
if [ -z "$value" ]; then
echo "ERROR: Could not resolve $var_name ($description) in resource group '$RG_NAME'." >&2
echo "Confirm 'azd provision' completed and the resource exists." >&2
exit 1
fi
azd env set $AZD_ENV_ARGS "$var_name" "$value"
echo "$var_name: set to $value"
}
echo "Resource group: $RG_NAME"
set_if_missing \
"AZURE_CONTAINER_REGISTRY_ENDPOINT" \
"container registry login server" \
az acr list --resource-group "$RG_NAME" --query "[0].loginServer" -o tsv
set_if_missing \
"AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID" \
"managed identity resource id" \
az identity list --resource-group "$RG_NAME" --query "[0].id" -o tsv
set_if_missing \
"MANAGED_IDENTITY_CLIENT_ID" \
"managed identity client id" \
az identity list --resource-group "$RG_NAME" --query "[0].clientId" -o tsv
echo "Aspire Container Apps environment variables are ready for 'azd deploy'."
references/recipes/bicep/errors.md›
# Bicep Validation Errors
| Error | Fix |
|-------|-----|
| `BCP035: Invalid type` | Check API version |
| `BCP037: Not a member` | Check resource schema |
| `BCP018: Expected character` | Fix syntax |
| `Module not found` | Check relative paths |
| `Template validation failed` | Review error details |
## Debug
```bash
az bicep build --file ./infra/main.bicep 2>&1
```
references/recipes/bicep/README.md›
# Bicep Validation
Validation steps for standalone Bicep deployments.
## Prerequisites
- `./infra/main.bicep` exists
- `./infra/main.parameters.json` exists
- Azure CLI authenticated
## Validation Steps
- [ ] 1. Core Validation (CLI, auth, build, validate, what-if) — run [`validate-deployment` script](../scripts/validate-deployment.sh)
- [ ] 2. Linting (optional)
- [ ] 3. Azure Policy Validation
## Validation Details
### 1. Core Validation Script
The core validation checks are a fixed, deterministic sequence — identical to the AZCLI
recipe. Run the shared **validate-deployment** helper instead of executing and parsing each
command by hand. It confirms the Azure CLI is installed and authenticated, compiles the Bicep
template (`az bicep build`), validates it against the target scope (`az deployment ...
validate`), and runs a what-if preview — printing a compact PASS/FAIL summary plus a what-if
change count (Create/Modify/Delete).
- Bash: [`../scripts/validate-deployment.sh`](../scripts/validate-deployment.sh)
- PowerShell: [`../scripts/validate-deployment.ps1`](../scripts/validate-deployment.ps1)
**Subscription scope:**
```bash
../scripts/validate-deployment.sh --scope sub --location <location>
```
```powershell
../scripts/validate-deployment.ps1 -Scope sub -Location <location>
```
**Resource group scope:**
```bash
../scripts/validate-deployment.sh --scope group --resource-group <rg-name>
```
```powershell
../scripts/validate-deployment.ps1 -Scope group -ResourceGroup <rg-name>
```
Defaults: `--template ./infra/main.bicep`, `--parameters ./infra/main.parameters.json`
(skipped if absent). Pass `--subscription <id>` to target a specific subscription.
**Interpreting results:**
- `OVERALL: PASS` — all checks passed; record the summary in Section 7 (Validation Proof).
- Any step `FAIL` — the script prints the failing command's error. If **Authenticated** fails,
run `az login`. Otherwise see [Error handling](./errors.md).
### 2. Linting (optional)
Use Bicep linter rules:
```bash
az bicep lint --file ./infra/main.bicep
```
### 3. Azure Policy Validation
See [Policy Validation Guide](../../policy-validation.md) for instructions on retrieving and validating Azure policies for your subscription.
## Checklist
| Check | Command | Pass |
|-------|---------|------|
| Core validation (CLI, auth, build, validate, what-if) | `validate-deployment` script | ☐ |
| Policies validated | MCP Policy tool | ☐ |
## References
- [Error handling](./errors.md)
## Next
All checks pass → **azure-deploy**
references/recipes/README.md›
# Recipes
Validation recipes for different infrastructure approaches.
| Recipe | When to Use |
|--------|-------------|
| [AZD](azd/README.md) | Projects using Azure Developer CLI |
| [AZCLI](azcli/README.md) | Projects using Azure CLI scripts |
| [Bicep](bicep/README.md) | Projects using Bicep templates |
| [Terraform](terraform/README.md) | Projects using Terraform |
references/recipes/scripts/validate-deployment.ps1›
<#
.SYNOPSIS
Runs the standard Azure CLI pre-deployment validation sequence for a Bicep
template and reports PASS/FAIL for each step. Shared by the AZCLI and Bicep
validation recipes.
.DESCRIPTION
Executes, in order:
1. az version - Azure CLI is installed
2. az account show - authenticated to Azure
3. az bicep build - template compiles cleanly
4. az deployment ... validate - template validates against the target scope
5. az deployment ... what-if - preview changes (Create/Modify/Delete summary)
Emits per-step PASS/FAIL lines and an OVERALL result.
Exit codes: 0 = every step passed; 1 = a validation step failed;
2 = usage / argument error.
.PARAMETER Scope
Deployment scope: 'sub' or 'group' (required).
.PARAMETER Location
Location (required when -Scope sub).
.PARAMETER ResourceGroup
Resource group name (required when -Scope group).
.PARAMETER Template
Bicep template path. Default: ./infra/main.bicep
.PARAMETER Parameters
Parameters file path. Default: ./infra/main.parameters.json
(skipped automatically if the file does not exist).
.PARAMETER Subscription
Subscription to target (optional).
.EXAMPLE
.\validate-deployment.ps1 -Scope sub -Location eastus
.EXAMPLE
.\validate-deployment.ps1 -Scope group -ResourceGroup my-rg `
-Template ./infra/main.bicep -Parameters ./infra/main.parameters.json
#>
param(
[ValidateSet("sub", "group")][string]$Scope,
[string]$Location,
[string]$ResourceGroup,
[string]$Template = "./infra/main.bicep",
[string]$Parameters = "./infra/main.parameters.json",
[string]$Subscription
)
# Validate arguments
if (-not $Scope) {
Write-Error "-Scope is required and must be 'sub' or 'group'."
exit 2
}
if ($Scope -eq "sub" -and -not $Location) {
Write-Error "-Location is required when -Scope is 'sub'."
exit 2
}
if ($Scope -eq "group" -and -not $ResourceGroup) {
Write-Error "-ResourceGroup is required when -Scope is 'group'."
exit 2
}
# Build shared argument arrays
$subArgs = @()
if ($Subscription) { $subArgs = @("--subscription", $Subscription) }
$paramArgs = @()
if (Test-Path $Parameters) {
$paramArgs = @("--parameters", $Parameters)
} else {
Write-Host "NOTE: parameters file '$Parameters' not found; validating without --parameters."
}
if ($Scope -eq "sub") {
$scopeTargetArgs = @("--location", $Location)
$scopeDesc = "subscription (location: $Location)"
} else {
$scopeTargetArgs = @("--resource-group", $ResourceGroup)
$scopeDesc = "resource group '$ResourceGroup'"
}
# Track overall result (0 = all passed, 1 = at least one failure).
$overall = 0
Write-Host "=== Azure deployment validation ==="
Write-Host "Template: $Template"
Write-Host "Scope: $scopeDesc"
Write-Host ""
# Step 1: Azure CLI installed
Write-Host "--- Step 1: Azure CLI installed (az version) ---"
az version *> $null
if ($LASTEXITCODE -eq 0) {
Write-Host "PASS: Azure CLI is installed."
} else {
Write-Host "FAIL: Azure CLI not found. Install it, then re-run."
Write-Host ""
Write-Host "OVERALL: FAIL"
exit 1
}
Write-Host ""
# Step 2: Authenticated
Write-Host "--- Step 2: Authenticated (az account show) ---"
$accountName = az account show @subArgs --query name -o tsv 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Host "PASS: Authenticated (subscription: $accountName)."
} else {
Write-Host "FAIL: Not logged in. Run 'az login' (and 'az account set --subscription <id>')."
$overall = 1
}
Write-Host ""
# Step 3: Bicep compilation
Write-Host "--- Step 3: Bicep compilation (az bicep build) ---"
$buildOutput = az bicep build --file $Template 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "PASS: Template compiles cleanly."
} else {
Write-Host "FAIL: Bicep compilation errors:"
Write-Host ($buildOutput | Out-String)
$overall = 1
}
Write-Host ""
# Step 4: Template validation
Write-Host "--- Step 4: Template validation (az deployment $Scope validate) ---"
$validateOutput = az deployment $Scope validate @scopeTargetArgs --template-file $Template @paramArgs @subArgs 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "PASS: Template validated against the target scope."
} else {
Write-Host "FAIL: Template validation errors:"
Write-Host ($validateOutput | Out-String)
$overall = 1
}
Write-Host ""
# Step 5: What-if preview
Write-Host "--- Step 5: What-if preview (az deployment $Scope what-if) ---"
$whatifOutput = az deployment $Scope what-if @scopeTargetArgs --template-file $Template @paramArgs @subArgs 2>&1
if ($LASTEXITCODE -eq 0) {
$lines = $whatifOutput | Out-String -Stream
$createCount = ($lines | Where-Object { $_ -match '^\s*\+ ' }).Count
$modifyCount = ($lines | Where-Object { $_ -match '^\s*~ ' }).Count
$deleteCount = ($lines | Where-Object { $_ -match '^\s*- ' }).Count
Write-Host "PASS: What-if completed. Changes -> Create: $createCount, Modify: $modifyCount, Delete: $deleteCount"
} else {
Write-Host "FAIL: What-if errors:"
Write-Host ($whatifOutput | Out-String)
$overall = 1
}
Write-Host ""
# Overall result (drives the exit code)
if ($overall -eq 0) {
Write-Host "OVERALL: PASS"
} else {
Write-Host "OVERALL: FAIL"
}
exit $overall
references/recipes/scripts/validate-deployment.sh›
#!/usr/bin/env bash
# validate-deployment.sh
# Runs the standard Azure CLI pre-deployment validation sequence for a Bicep
# template and reports PASS/FAIL for each step. Shared by the AZCLI and Bicep
# validation recipes.
#
# Steps (in order):
# 1. az version - Azure CLI is installed
# 2. az account show - authenticated to Azure
# 3. az bicep build - template compiles cleanly
# 4. az deployment ... validate - template validates against the target scope
# 5. az deployment ... what-if - preview changes (with a Create/Modify/Delete summary)
#
# Usage:
# ./validate-deployment.sh --scope sub --location <location> [options]
# ./validate-deployment.sh --scope group --resource-group <rg-name> [options]
#
# Options:
# --scope <sub|group> Deployment scope (required)
# --location <location> Location (required when --scope sub)
# --resource-group <name> Resource group (required when --scope group)
# --template <path> Bicep template (default: ./infra/main.bicep)
# --parameters <path> Parameters file (default: ./infra/main.parameters.json;
# skipped automatically if the file does not exist)
# --subscription <id> Subscription to target (optional)
#
# Examples:
# ./validate-deployment.sh --scope sub --location eastus
# ./validate-deployment.sh --scope group --resource-group my-rg \
# --template ./infra/main.bicep --parameters ./infra/main.parameters.json
#
# Exit codes:
# 0 - every validation step passed
# 1 - a validation step failed
# 2 - usage / argument error (unknown or valueless option, missing required flag)
set -uo pipefail
# Ensure an option that consumes a value actually has one ($@ = remaining args).
need_val() {
[ "$#" -ge 2 ] || { echo "ERROR: $1 requires a value." >&2; exit 2; }
}
SCOPE=""
LOCATION=""
RESOURCE_GROUP=""
TEMPLATE="./infra/main.bicep"
PARAMETERS="./infra/main.parameters.json"
SUBSCRIPTION=""
while [ $# -gt 0 ]; do
case "$1" in
--scope) need_val "$@"; SCOPE="$2"; shift 2 ;;
--location) need_val "$@"; LOCATION="$2"; shift 2 ;;
--resource-group) need_val "$@"; RESOURCE_GROUP="$2"; shift 2 ;;
--template) need_val "$@"; TEMPLATE="$2"; shift 2 ;;
--parameters) need_val "$@"; PARAMETERS="$2"; shift 2 ;;
--subscription) need_val "$@"; SUBSCRIPTION="$2"; shift 2 ;;
-h|--help)
grep '^#' "$0" | grep -v '^#!' | sed 's/^# \{0,1\}//'
exit 0 ;;
*)
echo "Unknown argument: $1" >&2
exit 2 ;;
esac
done
# Validate arguments
if [ "$SCOPE" != "sub" ] && [ "$SCOPE" != "group" ]; then
echo "ERROR: --scope must be 'sub' or 'group'." >&2
exit 2
fi
if [ "$SCOPE" = "sub" ] && [ -z "$LOCATION" ]; then
echo "ERROR: --location is required when --scope is 'sub'." >&2
exit 2
fi
if [ "$SCOPE" = "group" ] && [ -z "$RESOURCE_GROUP" ]; then
echo "ERROR: --resource-group is required when --scope is 'group'." >&2
exit 2
fi
# Build shared argument arrays
SUB_ARGS=()
[ -n "$SUBSCRIPTION" ] && SUB_ARGS=(--subscription "$SUBSCRIPTION")
PARAM_ARGS=()
if [ -f "$PARAMETERS" ]; then
PARAM_ARGS=(--parameters "$PARAMETERS")
else
echo "NOTE: parameters file '$PARAMETERS' not found; validating without --parameters."
fi
if [ "$SCOPE" = "sub" ]; then
SCOPE_TARGET_ARGS=(--location "$LOCATION")
SCOPE_DESC="subscription (location: $LOCATION)"
else
SCOPE_TARGET_ARGS=(--resource-group "$RESOURCE_GROUP")
SCOPE_DESC="resource group '$RESOURCE_GROUP'"
fi
# Track overall result (0 = all passed, 1 = at least one failure).
OVERALL=0
echo "=== Azure deployment validation ==="
echo "Template: $TEMPLATE"
echo "Scope: $SCOPE_DESC"
echo ""
# Step 1: Azure CLI installed
echo "--- Step 1: Azure CLI installed (az version) ---"
if az version >/dev/null 2>&1; then
echo "PASS: Azure CLI is installed."
else
echo "FAIL: Azure CLI not found. Install it, then re-run."
# Nothing else can run without the CLI.
echo ""
echo "OVERALL: FAIL"
exit 1
fi
echo ""
# Step 2: Authenticated
echo "--- Step 2: Authenticated (az account show) ---"
if ACCOUNT_NAME=$(az account show "${SUB_ARGS[@]}" --query name -o tsv 2>/dev/null); then
echo "PASS: Authenticated (subscription: ${ACCOUNT_NAME:-unknown})."
else
echo "FAIL: Not logged in. Run 'az login' (and 'az account set --subscription <id>')."
OVERALL=1
fi
echo ""
# Step 3: Bicep compilation
echo "--- Step 3: Bicep compilation (az bicep build) ---"
BUILD_OUTPUT=$(az bicep build --file "$TEMPLATE" 2>&1)
BUILD_RC=$?
if [ $BUILD_RC -eq 0 ]; then
echo "PASS: Template compiles cleanly."
else
echo "FAIL: Bicep compilation errors:"
echo "$BUILD_OUTPUT"
OVERALL=1
fi
echo ""
# Step 4: Template validation
echo "--- Step 4: Template validation (az deployment $SCOPE validate) ---"
VALIDATE_OUTPUT=$(az deployment "$SCOPE" validate "${SCOPE_TARGET_ARGS[@]}" \
--template-file "$TEMPLATE" \
"${PARAM_ARGS[@]}" "${SUB_ARGS[@]}" 2>&1)
VALIDATE_RC=$?
if [ $VALIDATE_RC -eq 0 ]; then
echo "PASS: Template validated against the target scope."
else
echo "FAIL: Template validation errors:"
echo "$VALIDATE_OUTPUT"
OVERALL=1
fi
echo ""
# Step 5: What-if preview
echo "--- Step 5: What-if preview (az deployment $SCOPE what-if) ---"
WHATIF_OUTPUT=$(az deployment "$SCOPE" what-if "${SCOPE_TARGET_ARGS[@]}" \
--template-file "$TEMPLATE" \
"${PARAM_ARGS[@]}" "${SUB_ARGS[@]}" 2>&1)
WHATIF_RC=$?
if [ $WHATIF_RC -eq 0 ]; then
CREATE_COUNT=$(echo "$WHATIF_OUTPUT" | grep -c '^[[:space:]]*+ ')
MODIFY_COUNT=$(echo "$WHATIF_OUTPUT" | grep -c '^[[:space:]]*~ ')
DELETE_COUNT=$(echo "$WHATIF_OUTPUT" | grep -c '^[[:space:]]*- ')
echo "PASS: What-if completed. Changes -> Create: $CREATE_COUNT, Modify: $MODIFY_COUNT, Delete: $DELETE_COUNT"
else
echo "FAIL: What-if errors:"
echo "$WHATIF_OUTPUT"
OVERALL=1
fi
echo ""
# Overall result
if [ $OVERALL -eq 0 ]; then
echo "OVERALL: PASS"
else
echo "OVERALL: FAIL"
fi
exit $OVERALL
references/recipes/terraform/errors.md›
# Terraform Validation Errors
| Error | Fix |
|-------|-----|
| `Backend init failed` | Check storage account access |
| `Provider version conflict` | Update required_providers |
| `State lock failed` | Wait or force unlock |
| `Validation failed` | Check terraform validate output |
| `Error: Cycle:` | See [Cycle Errors](#cycle-errors) below |
## Cycle Errors
`terraform validate` reports a cycle when two or more resources reference each other's attributes, creating a circular dependency.
### Common Pattern: CORS Cross-Reference
Multi-service App Service deployments often introduce a cycle when the API's CORS configuration references the frontend hostname and the frontend's app settings reference the API hostname:
```
Error: Cycle: azurerm_linux_web_app.frontend, azurerm_linux_web_app.api
```
**Cause — circular attribute references:**
```hcl
# API references frontend.default_hostname in CORS
resource "azurerm_linux_web_app" "api" {
site_config {
cors {
allowed_origins = ["https://${azurerm_linux_web_app.frontend.default_hostname}"]
}
}
}
# Frontend references api.default_hostname in app_settings
resource "azurerm_linux_web_app" "frontend" {
app_settings = {
API_URL = "https://${azurerm_linux_web_app.api.default_hostname}"
}
}
```
### Fix Strategies
**Option A (recommended):** Use a Terraform variable for the frontend origin so CORS is restrictive by default and the cycle is broken. Define the variable with a sensible default and pass the real frontend URL after the first deployment:
```hcl
variable "frontend_origin" {
type = string
description = "Frontend origin for API CORS. Set after first deployment."
default = ""
}
resource "azurerm_linux_web_app" "api" {
site_config {
cors {
allowed_origins = var.frontend_origin != "" ? [var.frontend_origin] : ["*"]
support_credentials = var.frontend_origin != "" ? true : false
}
}
}
```
> ⚠️ **Warning:** If using `["*"]` as a temporary bootstrap value, you **must** set `frontend_origin` to the actual URL (e.g., `https://app-web-*.azurewebsites.net`) and re-run `terraform apply` in the same deployment session before reporting success. Do not leave wildcard CORS in a completed deployment.
**Option B:** Use `azurerm_app_service_custom_hostname_binding` or a `null_resource` with a `local-exec` provisioner to configure CORS after both resources are created, breaking the dependency chain.
**Option C:** Use `lifecycle { ignore_changes = [site_config[0].cors] }` on the API resource and configure CORS via a separate `azurerm_web_app_active_slot` or post-deployment script.
### After Fixing
1. Run `terraform fmt -recursive` to fix formatting
2. Re-run `terraform validate` to confirm the cycle is resolved
3. Run `terraform plan` to verify the configuration is correct
## Debug
```bash
TF_LOG=DEBUG terraform plan
```
references/recipes/terraform/README.md›
# Terraform Validation
Validation steps for Terraform deployments.
## Prerequisites
- `./infra/main.tf` exists
- State backend accessible
## Run the preflight script
Run the pre-built validation script instead of executing each check by hand. It runs the
full deterministic preflight sequence in one call and prints a compact **PASS / FAIL / SKIP**
summary plus captured error text for any failed step — jump straight to remediation without
re-parsing raw command output.
| Script | Purpose |
|--------|---------|
| [`scripts/validate-terraform.sh`](scripts/validate-terraform.sh) | Bash preflight runner |
| [`scripts/validate-terraform.ps1`](scripts/validate-terraform.ps1) | PowerShell preflight runner |
The script runs, in order: Terraform installed → Azure CLI installed → authenticated
(`az account show`) → `terraform init` → `fmt -check` → `validate` → `plan` →
`state list` → Go-style `{{ .Env.* }}` template-variable scan → `main.tfvars.json`
JSON-syntax check. A subscription-selection step is added when a subscription id is
supplied. It runs **every** check even if an earlier one fails, and exits non-zero when
any step fails.
**Usage:**
```bash
./scripts/validate-terraform.sh [infra-dir] [subscription-id] # infra-dir defaults to ./infra
```
```powershell
.\scripts\validate-terraform.ps1 [-InfraDir <path>] [-SubscriptionId <id>]
```
**Examples:**
```bash
./scripts/validate-terraform.sh # validate ./infra
./scripts/validate-terraform.sh ./infra 00000000-0000-0000-0000-000000000000
```
```powershell
.\scripts\validate-terraform.ps1 -InfraDir ./infra
```
**Reading the output:** the summary table lists every step as `PASS`, `FAIL`, or `SKIP`
(skipped when a prerequisite such as Terraform or the infra directory is missing). Each
`FAIL` is expanded in a **FAILURE DETAILS** section with the captured error text. Fix
failed steps using the guidance below, then re-run the script.
## Remediation
The script only **runs and reports** — fixing failures is manual. Guidance per step:
### Terraform / Azure CLI not installed
- Terraform: see https://developer.hashicorp.com/terraform/install
- Azure CLI: `mcp_azure_mcp_extension_cli_install(cli-type: "az")`
### Not authenticated / wrong subscription
```bash
az login
az account set --subscription <subscription-id>
```
### Format check failed
```bash
terraform fmt -recursive
```
### Init / validate / plan / state failures
Read the captured error text in the script output, then consult
[Error handling](./errors.md).
### Azure Policy Validation
The script does not cover policy checks. See
[Policy Validation Guide](../../policy-validation.md) for retrieving and validating Azure
policies for your subscription.
### Template Variable Resolution (AZD+Terraform)
> ⚠️ **CRITICAL for azd+Terraform projects.** azd substitutes `${VAR}` references in
> `main.tfvars.json` via envsubst, but does NOT interpolate Go-style template variables
> (`{{ .Env.* }}`). Unresolved Go-style template strings passed to Terraform cause
> cascading deployment failures, state conflicts, and timeouts.
When the template-variable scan reports `FAIL`:
1. **Fix the syntax** in `main.tfvars.json` — replace `{{ .Env.VAR }}` with `${VAR}`:
```json
{ "environment_name": "${AZURE_ENV_NAME}", "location": "${AZURE_LOCATION}" }
```
2. For additional variables, use **`TF_VAR_*` environment variables**:
```bash
azd env set TF_VAR_environment_name "$(azd env get-value AZURE_ENV_NAME)"
```
3. **Verify** that `variables.tf` declares all required variables.
4. **Re-run** the script to confirm `terraform validate` / `plan` and the scan now pass.
> Prefer putting static defaults in `variables.tf` `default` values. Using `terraform.tfvars`
> (HCL) for static defaults is acceptable if your team prefers it; this restriction is
> specifically about avoiding Go-style template expressions in `.tfvars.json` files.
## References
- [Error handling](./errors.md)
## Next
All checks pass → **azure-deploy**
references/recipes/terraform/scripts/validate-terraform.ps1›
<#
.SYNOPSIS
Runs the Terraform pre-deployment validation preflight sequence and prints a
compact PASS/FAIL/SKIP summary plus captured error text for any failed step.
.DESCRIPTION
Runs every check even if an earlier one fails, so you get a complete verdict in
a single call. It never fixes anything - it only runs and reports, so you can
jump straight to remediation for any failed step.
Steps: terraform present, az present, authenticated, init, fmt -check, validate,
plan, state list, Go-style {{ .Env.* }} template-variable scan.
.PARAMETER InfraDir
Path to the Terraform infra directory (default: ./infra).
.PARAMETER SubscriptionId
Optional subscription to select before checks.
.EXAMPLE
.\validate-terraform.ps1
# Validate ./infra
.EXAMPLE
.\validate-terraform.ps1 -InfraDir ./infra -SubscriptionId 00000000-0000-0000-0000-000000000000
# Validate an explicit directory against a specific subscription
.NOTES
Exit code: 0 when every non-skipped step passes, 1 when any step fails.
#>
param(
[string]$InfraDir = "./infra",
[string]$SubscriptionId
)
$ErrorActionPreference = "Continue"
$steps = [System.Collections.Generic.List[object]]::new()
function Add-Result {
param([string]$Name, [string]$Status, [string]$ErrorText = "")
# Results are collected here and rendered once in the summary at the end.
$steps.Add([pscustomobject]@{ Name = $Name; Status = $Status; Error = $ErrorText })
}
function Test-Command {
param([string]$Name)
return [bool](Get-Command $Name -ErrorAction SilentlyContinue)
}
Write-Host "Terraform validation preflight - infra dir: $InfraDir"
Write-Host ""
# --- 1. Terraform installed --------------------------------------------------
if (Test-Command "terraform") {
Add-Result "Terraform installed" "PASS"
} else {
Add-Result "Terraform installed" "FAIL" "terraform not found on PATH. Install: https://developer.hashicorp.com/terraform/install"
}
# --- 2. Azure CLI installed --------------------------------------------------
$hasAz = Test-Command "az"
if ($hasAz) {
Add-Result "Azure CLI installed" "PASS"
} else {
Add-Result "Azure CLI installed" "FAIL" "az not found on PATH. Install the Azure CLI: mcp_azure_mcp_extension_cli_install(cli-type: `"az`")"
}
# --- 3. Authentication -------------------------------------------------------
if ($hasAz) {
if ($SubscriptionId) {
$subOut = az account set --subscription $SubscriptionId 2>&1
if ($LASTEXITCODE -eq 0) {
Add-Result "Select subscription" "PASS"
} else {
Add-Result "Select subscription" "FAIL" ($subOut | Out-String).Trim()
}
}
$accountOut = az account show -o none 2>&1
if ($LASTEXITCODE -eq 0) {
Add-Result "Authenticated (az account show)" "PASS"
} else {
Add-Result "Authenticated (az account show)" "FAIL" ($accountOut | Out-String).Trim()
}
} else {
Add-Result "Authenticated (az account show)" "SKIP" "Azure CLI not installed"
}
# --- infra dir presence gate -------------------------------------------------
$haveTf = (Test-Command "terraform") -and (Test-Path -Path $InfraDir -PathType Container)
function Invoke-Tf {
param([string]$Name, [string[]]$Args)
if (-not $haveTf) {
Add-Result $Name "SKIP" "terraform unavailable or infra dir '$InfraDir' not found"
return
}
Push-Location $InfraDir
try {
# Stream output to a temp file so large output (e.g. terraform plan) is
# not held in memory; only read it back when the command fails.
$tmp = New-TemporaryFile
& terraform @Args *> $tmp.FullName
if ($LASTEXITCODE -eq 0) {
Add-Result $Name "PASS"
} else {
$content = Get-Content -Raw $tmp.FullName
if ($null -eq $content) { $content = "" }
Add-Result $Name "FAIL" $content.Trim()
}
Remove-Item $tmp.FullName -ErrorAction SilentlyContinue
} finally {
Pop-Location
}
}
# --- 4. Initialize -----------------------------------------------------------
Invoke-Tf "terraform init" @("init", "-input=false")
# --- 5. Format check ---------------------------------------------------------
Invoke-Tf "terraform fmt -check" @("fmt", "-check", "-recursive")
# --- 6. Validate syntax ------------------------------------------------------
Invoke-Tf "terraform validate" @("validate")
# --- 7. Plan preview ---------------------------------------------------------
Invoke-Tf "terraform plan" @("plan", "-input=false", "-out=tfplan")
# --- 8. State backend --------------------------------------------------------
Invoke-Tf "terraform state list" @("state", "list")
# --- 9. Go-style template-variable scan --------------------------------------
if (Test-Path -Path $InfraDir -PathType Container) {
$hits = Get-ChildItem -Path $InfraDir -Recurse -Include "*.tf", "*.tfvars.json" -ErrorAction SilentlyContinue |
Select-String -Pattern '{{ *\.Env\.' |
ForEach-Object { "{0}:{1}:{2}" -f $_.Path, $_.LineNumber, $_.Line.Trim() }
if ($hits) {
$detail = "Found unresolved Go-style template variables - replace {{ .Env.VAR }} with `${VAR} (azd envsubst format):`n" + ($hits -join "`n")
Add-Result "Template-variable scan ({{ .Env.* }})" "FAIL" $detail
} else {
Add-Result "Template-variable scan ({{ .Env.* }})" "PASS"
}
} else {
Add-Result "Template-variable scan ({{ .Env.* }})" "SKIP" "infra dir '$InfraDir' not found"
}
# --- 10. main.tfvars.json JSON syntax ----------------------------------------
$tfvars = Join-Path $InfraDir "main.tfvars.json"
if (Test-Path -Path $tfvars -PathType Leaf) {
try {
Get-Content -Raw $tfvars | ConvertFrom-Json -ErrorAction Stop | Out-Null
Add-Result "main.tfvars.json is valid JSON" "PASS"
} catch {
Add-Result "main.tfvars.json is valid JSON" "FAIL" $_.Exception.Message
}
} else {
Add-Result "main.tfvars.json is valid JSON" "SKIP" "$tfvars not found"
}
# --- summary -----------------------------------------------------------------
Write-Host ""
Write-Host "==================== SUMMARY ===================="
"{0,-40} {1}" -f "STEP", "RESULT" | Write-Host
"{0,-40} {1}" -f "----", "------" | Write-Host
foreach ($s in $steps) {
"{0,-40} {1}" -f $s.Name, $s.Status | Write-Host
}
Write-Host "================================================="
$failed = @($steps | Where-Object { $_.Status -eq "FAIL" })
if ($failed.Count -gt 0) {
Write-Host ""
Write-Host "----- FAILURE DETAILS -----"
foreach ($s in $failed) {
Write-Host ""
Write-Host "### $($s.Name)"
Write-Host $s.Error
}
Write-Host ""
Write-Host "RESULT: $($failed.Count) step(s) failed. See remediation guidance in README.md."
exit 1
}
Write-Host ""
Write-Host "RESULT: All checks passed. Ready for azure-deploy."
exit 0
references/recipes/terraform/scripts/validate-terraform.sh›
#!/usr/bin/env bash
# validate-terraform.sh
# Runs the Terraform pre-deployment validation preflight sequence and prints a
# compact PASS/FAIL/SKIP summary plus the captured error text for any failed step.
#
# The script runs every check even if an earlier one fails, so you get a complete
# verdict in a single call. It never fixes anything - it only runs and reports, so
# you can jump straight to remediation for any failed step.
#
# Steps: terraform present, az present, authenticated, init, fmt -check, validate,
# plan, state list, Go-style {{ .Env.* }} template-variable scan.
#
# Usage:
# ./validate-terraform.sh [infra-dir] [subscription-id]
#
# Arguments:
# infra-dir Path to the Terraform infra directory (default: ./infra).
# subscription-id Optional subscription to select before checks.
#
# Examples:
# ./validate-terraform.sh # Validate ./infra
# ./validate-terraform.sh ./infra # Validate an explicit directory
# ./validate-terraform.sh ./infra 00000000-0000-0000-0000-000000000000
#
# Exit code: 0 when every non-skipped step passes, 1 when any step fails.
set -uo pipefail
INFRA_DIR="${1:-./infra}"
SUBSCRIPTION_ID="${2:-}"
# --- result tracking ---------------------------------------------------------
STEP_NAMES=()
STEP_STATUS=() # PASS | FAIL | SKIP
STEP_ERRORS=() # captured error text (empty unless FAIL)
record() {
# record <name> <status> [error-text]
# Results are collected here and rendered once in the summary at the end.
STEP_NAMES+=("$1")
STEP_STATUS+=("$2")
STEP_ERRORS+=("${3:-}")
}
echo "Terraform validation preflight - infra dir: $INFRA_DIR"
echo ""
# --- 1. Terraform installed --------------------------------------------------
if command -v terraform >/dev/null 2>&1; then
record "Terraform installed" "PASS"
else
record "Terraform installed" "FAIL" "terraform not found on PATH. Install: https://developer.hashicorp.com/terraform/install"
fi
# --- 2. Azure CLI installed --------------------------------------------------
if command -v az >/dev/null 2>&1; then
record "Azure CLI installed" "PASS"
else
record "Azure CLI installed" "FAIL" "az not found on PATH. Install the Azure CLI: mcp_azure_mcp_extension_cli_install(cli-type: \"az\")"
fi
# --- 3. Authentication -------------------------------------------------------
if command -v az >/dev/null 2>&1; then
if [ -n "$SUBSCRIPTION_ID" ]; then
if SUB_OUT=$(az account set --subscription "$SUBSCRIPTION_ID" 2>&1); then
record "Select subscription" "PASS"
else
record "Select subscription" "FAIL" "$SUB_OUT"
fi
fi
if ACCOUNT_OUT=$(az account show -o none 2>&1); then
record "Authenticated (az account show)" "PASS"
else
record "Authenticated (az account show)" "FAIL" "$ACCOUNT_OUT"
fi
else
record "Authenticated (az account show)" "SKIP" "Azure CLI not installed"
fi
# --- infra dir presence gate -------------------------------------------------
HAVE_TF=false
if command -v terraform >/dev/null 2>&1 && [ -d "$INFRA_DIR" ]; then
HAVE_TF=true
fi
run_tf() {
# run_tf <name> <terraform args...>
local name="$1"; shift
if [ "$HAVE_TF" != true ]; then
record "$name" "SKIP" "terraform unavailable or infra dir '$INFRA_DIR' not found"
return
fi
# Stream output to a temp file so large output (e.g. terraform plan) is not
# held in memory; only read it back when the command fails.
local tmp
tmp=$(mktemp)
if (cd "$INFRA_DIR" && terraform "$@") >"$tmp" 2>&1; then
record "$name" "PASS"
else
record "$name" "FAIL" "$(cat "$tmp")"
fi
rm -f "$tmp"
}
# --- 4. Initialize -----------------------------------------------------------
run_tf "terraform init" init -input=false
# --- 5. Format check ---------------------------------------------------------
run_tf "terraform fmt -check" fmt -check -recursive
# --- 6. Validate syntax ------------------------------------------------------
run_tf "terraform validate" validate
# --- 7. Plan preview ---------------------------------------------------------
run_tf "terraform plan" plan -input=false -out=tfplan
# --- 8. State backend --------------------------------------------------------
run_tf "terraform state list" state list
# --- 9. Go-style template-variable scan --------------------------------------
if [ -d "$INFRA_DIR" ]; then
TEMPLATE_HITS=$(grep -rn '{{ *\.Env\.' "$INFRA_DIR" --include='*.tf' --include='*.tfvars.json' 2>/dev/null || true)
if [ -n "$TEMPLATE_HITS" ]; then
record "Template-variable scan ({{ .Env.* }})" "FAIL" \
"Found unresolved Go-style template variables - replace {{ .Env.VAR }} with \${VAR} (azd envsubst format):
$TEMPLATE_HITS"
else
record "Template-variable scan ({{ .Env.* }})" "PASS"
fi
else
record "Template-variable scan ({{ .Env.* }})" "SKIP" "infra dir '$INFRA_DIR' not found"
fi
# --- 10. main.tfvars.json JSON syntax ----------------------------------------
TFVARS="$INFRA_DIR/main.tfvars.json"
if [ -f "$TFVARS" ]; then
if command -v python3 >/dev/null 2>&1; then
if JSON_ERR=$(python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$TFVARS" 2>&1); then
record "main.tfvars.json is valid JSON" "PASS"
else
record "main.tfvars.json is valid JSON" "FAIL" "$JSON_ERR"
fi
elif command -v jq >/dev/null 2>&1; then
if JSON_ERR=$(jq empty "$TFVARS" 2>&1); then
record "main.tfvars.json is valid JSON" "PASS"
else
record "main.tfvars.json is valid JSON" "FAIL" "$JSON_ERR"
fi
else
record "main.tfvars.json is valid JSON" "SKIP" "no JSON parser (python3/jq) available"
fi
else
record "main.tfvars.json is valid JSON" "SKIP" "$TFVARS not found"
fi
# --- summary -----------------------------------------------------------------
echo ""
echo "==================== SUMMARY ===================="
printf '%-40s %s\n' "STEP" "RESULT"
printf '%-40s %s\n' "----" "------"
FAILED=0
for i in "${!STEP_NAMES[@]}"; do
printf '%-40s %s\n' "${STEP_NAMES[$i]}" "${STEP_STATUS[$i]}"
[ "${STEP_STATUS[$i]}" = "FAIL" ] && FAILED=$((FAILED + 1))
done
echo "================================================="
if [ "$FAILED" -gt 0 ]; then
echo ""
echo "----- FAILURE DETAILS -----"
for i in "${!STEP_NAMES[@]}"; do
if [ "${STEP_STATUS[$i]}" = "FAIL" ]; then
echo ""
echo "### ${STEP_NAMES[$i]}"
echo "${STEP_ERRORS[$i]}"
fi
done
echo ""
echo "RESULT: $FAILED step(s) failed. See remediation guidance in README.md."
exit 1
fi
echo ""
echo "RESULT: All checks passed. Ready for azure-deploy."
exit 0
references/region-availability.md›
# Azure Region Availability Reference
> **AUTHORITATIVE SOURCE** — Consult this file BEFORE recommending any region.
>
> Official reference: https://azure.microsoft.com/en-us/explore/global-infrastructure/products-by-region/table
## How to Use
1. Check if your architecture includes any **limited availability** services below
2. If yes → refer to the table or use the MCP tool to list supported regions with sufficient quota for that service, and only offer regions that support ALL services
3. If all services are "available everywhere" → offer common regions
## MCP Tools Used
| Tool | Purpose |
|------|---------|
| `mcp_azure_mcp_quota` | Check Azure region availability and quota by setting `command` to `quota_usage_check` or `quota_region_availability_list` |
---
## Services with LIMITED Region Availability
### Azure Static Web Apps (SWA)
⚠️ **NOT available in many common regions**
| ✅ Available | ❌ NOT Available (will FAIL) |
|-------------|------------------------------|
| `westus2` | `eastus` |
| `centralus` | `northeurope` |
| `eastus2` | `southeastasia` |
| `westeurope` | `uksouth` |
| `eastasia` | `canadacentral` |
| | `australiaeast` |
| | `westus3` |
---
### Azure OpenAI
⚠️ **Very limited — varies by model**
| Region | GPT-4o | GPT-4 | GPT-3.5 | Embeddings |
|--------|:------:|:-----:|:-------:|:----------:|
| `eastus` | ✅ | ✅ | ✅ | ✅ |
| `eastus2` | ✅ | ✅ | ✅ | ✅ |
| `westus` | ⚠️ | ⚠️ | ✅ | ✅ |
| `westus3` | ✅ | ⚠️ | ✅ | ✅ |
| `southcentralus` | ✅ | ✅ | ✅ | ✅ |
| `swedencentral` | ✅ | ✅ | ✅ | ✅ |
| `westeurope` | ⚠️ | ✅ | ✅ | ✅ |
> Check https://learn.microsoft.com/azure/ai-services/openai/concepts/models for current model availability.
---
### Azure Kubernetes Service (AKS)
It has limited quota in some regions, to get available regions with enough quota, use `mcp_azure_mcp_quota` tool.
---
### Azure Database for PostgreSQL
It has limited quota in some regions, to get available regions with enough quota, use `mcp_azure_mcp_quota` tool.
---
## Services Available in Most Regions
These services are available in all major Azure regions — no special consideration needed:
- **Container Apps**
- **Azure Functions**
- **App Service**
- **Azure SQL Database**
- **Cosmos DB**
- **Key Vault**
- **Storage Account**
- **Service Bus**
- **Event Grid**
- **Application Insights / Log Analytics**
---
## Common Architecture Patterns
| Pattern | Recommended Regions |
|---------|---------------------|
| SWA only | `westus2`, `centralus`, `eastus2`, `westeurope`, `eastasia` |
| SWA + backend services | `westus2`, `centralus`, `eastus2`, `westeurope`, `eastasia` |
| Container Apps (no SWA) | `eastus`, `eastus2`, `westus2`, `centralus`, `westeurope` |
| With Azure OpenAI (GPT-4o/4/3.5 + embeddings) | `eastus`, `eastus2`, `swedencentral` |
| SWA + Azure OpenAI (GPT-4o/4/3.5 + embeddings) | `eastus2` (only region with full SWA + model overlap) |
---
**Last updated:** 2026-03-02
references/role-verification.md›
# Role Assignment Verification
Verify that all RBAC role assignments in the generated infrastructure are correct and sufficient before deployment. Incorrect or missing roles are a common cause of runtime failures.
## When to Verify
After build verification (step 4) and **before** recording proof (step 6). Role issues surface as cryptic auth errors during deployment — catching them here saves debugging time.
## Verification Checklist
Review every resource-to-identity relationship in the generated Bicep/Terraform:
| Check | How |
|-------|-----|
| **Every service identity has roles** | Each app with a managed identity must have at least one role assignment |
| **Roles match data operations** | Use service-specific **data-plane** roles for data access (see mapping table below); use generic Reader/Contributor/Owner only for management-plane operations |
| **Scope is least privilege** | Roles scoped to specific resources, not resource groups or subscriptions |
| **No missing roles** | Cross-check app code operations against assigned roles (see table below) |
| **Local dev identity has roles** | If testing locally, the user's identity needs equivalent roles via `az login` |
## Common Service-to-Role Mapping
| Service Operation | Required Role | Common Mistake |
|-------------------|---------------|----------------|
| Read blobs | Storage Blob Data Reader | Using generic Reader (no data access) |
| Read + write blobs | Storage Blob Data Contributor | Missing write permission |
| Generate SAS via user delegation | Storage Blob Delegator + Data Reader/Contributor | Forgetting Delegator role |
| Read Key Vault secrets | Key Vault Secrets User | Using Key Vault Reader (no secret access) |
| Read + write Cosmos DB | Cosmos DB Built-in Data Contributor | Using generic Contributor |
| Send Service Bus messages | Azure Service Bus Data Sender | Using generic Contributor |
| Read queues | Storage Queue Data Reader | Using Blob role for queues |
## How to Verify (Static Code Review)
Review the generated Bicep/Terraform files directly — do **not** query live Azure state here. For each role assignment resource in your infrastructure code:
1. Identify the **principal** (which managed identity)
2. Identify the **role** (which role definition)
3. Identify the **scope** (which target resource)
4. Cross-check against the app code to confirm the role grants the required data-plane access
> 💡 **Tip:** Search your Bicep for `Microsoft.Authorization/roleAssignments` or your Terraform for `azurerm_role_assignment` to find all role assignments.
> ⚠️ **Live role verification** (querying Azure for actually provisioned roles) is handled by **azure-deploy** step 8 as a post-deployment check. This step is a static code review only.
## Decision Tree
```
For each app identity in the generated infrastructure:
├── Has role assignments?
│ ├── No → Add required role assignments to Bicep/Terraform
│ └── Yes → Check each role:
│ ├── Role matches code operations? → ✅ OK
│ ├── Role too broad? → Narrow to least privilege
│ └── Role insufficient? → Upgrade or add missing role
│
For local testing:
├── User identity has equivalent roles?
│ ├── No → Grant roles via CLI or inform user
│ └── Yes → ✅ Ready for functional verification
```
> ⚠️ **Warning:** Generic roles like `Contributor` or `Reader` do **not** include data-plane access. For example, `Contributor` on a Storage Account cannot read blobs — you need `Storage Blob Data Contributor`. This is the most common RBAC mistake.
## Record in Plan
After role verification, update `.azure/deployment-plan.md`:
```markdown
## Role Assignment Verification
- Status: Verified / Issues Found
- Identities checked: <list of app identities>
- Roles confirmed: <list of role assignments>
- Issues: <any missing or incorrect roles fixed>
```
references/scripts/scan-aspire-functions-secrets.ps1›
<#
.SYNOPSIS
Aspire + Azure Functions secret-storage pre-provisioning scan.
.DESCRIPTION
Decides whether the AzureWebJobsSecretStorageType=Files fix is required by
scanning C# source for the Aspire Functions builder call and the setting.
Logic:
1. Find *.cs files that call AddAzureFunctionsProject.
2. For each, check whether the same file already sets AzureWebJobsSecretStorageType.
3. Files with the call but missing the setting -> fix required.
Prints a single verdict — NOT APPLICABLE, ALREADY CONFIGURED, or FIX REQUIRED
(with the matching file(s) and line(s)). Exit code is 0 for every verdict;
a non-zero exit only indicates a usage/environment error.
.PARAMETER Path
Directory to scan. Defaults to the current directory.
.EXAMPLE
.\scan-aspire-functions-secrets.ps1
# Scan the current directory
.EXAMPLE
.\scan-aspire-functions-secrets.ps1 -Path ./src
# Scan a specific directory
#>
param(
[string]$Path = "."
)
# Leave $ErrorActionPreference at its default ("Continue") so that Write-Error
# below is non-terminating and the explicit `exit` codes are honored.
if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
Write-Error "'$Path' is not a directory."
exit 2
}
$call = "AddAzureFunctionsProject"
$setting = "AzureWebJobsSecretStorageType"
# Collect *.cs files that reference AddAzureFunctionsProject.
$matches = Get-ChildItem -Path $Path -Recurse -File -Filter "*.cs" |
Where-Object { Select-String -Path $_.FullName -SimpleMatch -Pattern $call -Quiet }
if (-not $matches) {
Write-Output "VERDICT: NOT APPLICABLE"
Write-Output "No '$call' call found in any *.cs file under '$Path'."
Write-Output "The Functions secret-storage check does not apply - skip it."
exit 0
}
# Partition matching files by whether they already configure the setting.
$needsFix = @()
$configured = @()
foreach ($file in $matches) {
if (Select-String -Path $file.FullName -SimpleMatch -Pattern $setting -Quiet) {
$configured += $file
} else {
$needsFix += $file
}
}
if ($needsFix.Count -eq 0) {
Write-Output "VERDICT: ALREADY CONFIGURED"
Write-Output "Every file that calls '$call' already sets '$setting':"
foreach ($file in $configured) {
$line = (Select-String -Path $file.FullName -SimpleMatch -Pattern $setting |
Select-Object -First 1).LineNumber
Write-Output " - $($file.FullName) (line $line)"
}
Write-Output "No change required."
exit 0
}
Write-Output "VERDICT: FIX REQUIRED"
Write-Output "The following file(s) call '$call' but do NOT set '$setting':"
foreach ($file in $needsFix) {
$line = (Select-String -Path $file.FullName -SimpleMatch -Pattern $call |
Select-Object -First 1).LineNumber
Write-Output " - $($file.FullName) (line $line)"
}
Write-Output ""
Write-Output "Add .WithEnvironment(`"$setting`", `"Files`") to the AddAzureFunctionsProject"
Write-Output "builder chain in each file above BEFORE running 'azd provision'."
exit 0
references/scripts/scan-aspire-functions-secrets.sh›
#!/usr/bin/env bash
# scan-aspire-functions-secrets.sh
# Aspire + Azure Functions secret-storage pre-provisioning scan.
#
# Decides whether the AzureWebJobsSecretStorageType=Files fix is required by
# scanning C# source for the Aspire Functions builder call and the setting.
#
# Logic:
# 1. Find *.cs files that call AddAzureFunctionsProject.
# 2. For each, check whether the same file already sets AzureWebJobsSecretStorageType.
# 3. Files with the call but missing the setting -> fix required.
#
# Usage:
# ./scan-aspire-functions-secrets.sh [directory]
#
# Examples:
# ./scan-aspire-functions-secrets.sh # Scan current directory
# ./scan-aspire-functions-secrets.sh ./src # Scan a specific directory
#
# Output: a single verdict — NOT APPLICABLE, ALREADY CONFIGURED, or FIX REQUIRED
# (with the matching file(s) and line(s)). Exit code is 0 for every verdict;
# a non-zero exit only indicates a usage/environment error.
set -euo pipefail
ROOT="${1:-.}"
if [ ! -d "$ROOT" ]; then
echo "ERROR: '$ROOT' is not a directory." >&2
exit 2
fi
CALL="AddAzureFunctionsProject"
SETTING="AzureWebJobsSecretStorageType"
# file_contains <fixed-string> <file>
# Returns 0 if the file contains the literal string, 1 if not.
# A grep read error (exit code 2) is fatal: this is a critical pre-provision
# scan, so a file we cannot read must not be silently reported as "no match".
file_contains() {
local pattern="$1" file="$2" rc
if grep -Fq -- "$pattern" "$file"; then
return 0
fi
rc=$?
if [ "$rc" -eq 2 ]; then
echo "ERROR: failed to read '$file' while scanning for '$pattern'." >&2
exit 3
fi
return 1
}
# first_line <fixed-string> <file>
# Prints the 1-based line number of the first literal match (or nothing).
first_line() {
grep -Fn -- "$1" "$2" | head -n1 | cut -d: -f1
}
# Collect *.cs files that reference AddAzureFunctionsProject into an array.
# find ... -print0 + read -d '' keeps paths intact even if they contain
# newlines or spaces (genuinely NUL-safe, unlike a newline-joined string).
matches=()
while IFS= read -r -d '' file; do
if file_contains "$CALL" "$file"; then
matches+=("$file")
fi
done < <(find "$ROOT" -type f -name "*.cs" -print0)
if [ "${#matches[@]}" -eq 0 ]; then
echo "VERDICT: NOT APPLICABLE"
echo "No '$CALL' call found in any *.cs file under '$ROOT'."
echo "The Functions secret-storage check does not apply — skip it."
exit 0
fi
# Partition matching files by whether they already configure the setting.
needs_fix=()
configured=()
for file in "${matches[@]}"; do
if file_contains "$SETTING" "$file"; then
configured+=("$file")
else
needs_fix+=("$file")
fi
done
if [ "${#needs_fix[@]}" -eq 0 ]; then
echo "VERDICT: ALREADY CONFIGURED"
echo "Every file that calls '$CALL' already sets '$SETTING':"
for file in "${configured[@]}"; do
echo " - $file (line $(first_line "$SETTING" "$file"))"
done
echo "No change required."
exit 0
fi
echo "VERDICT: FIX REQUIRED"
echo "The following file(s) call '$CALL' but do NOT set '$SETTING':"
for file in "${needs_fix[@]}"; do
echo " - $file (line $(first_line "$CALL" "$file"))"
done
echo ""
echo "Add .WithEnvironment(\"$SETTING\", \"Files\") to the AddAzureFunctionsProject"
echo "builder chain in each file above BEFORE running 'azd provision'."
exit 0
references/scripts/workflow.ps1›
<#
.SYNOPSIS
Walks the agent through the azure-validate workflow, one step at a time.
.PARAMETER WorkspacePath
Path to the workspace being validated (required).
.PARAMETER CompletedStep
The workflow step the agent just completed. Omit this on the first call to
start the workflow. The script records the value in
.azure/validate-status.json and returns the next action to take, along with
the value to pass as -CompletedStep on the next call.
#>
param(
[string]$WorkspacePath,
[string]$CompletedStep
)
enum ValidationStep {
None
LoadPlan
AddValidationSteps
RunValidation
BuildVerification
StaticRoleVerification
RecordProof
ResolveErrors
UpdateStatus
}
if (-not $WorkspacePath) {
Write-Error "WorkspacePath is required."
exit 2
}
if (-not (Test-Path -Path $WorkspacePath -PathType Container)) {
Write-Error "Error: WorkspacePath '$WorkspacePath' does not exist or is not a directory."
exit 2
}
# Resolve the step the agent just completed.
# Omitting -CompletedStep signals the start of the workflow (None).
$step = [ValidationStep]::None
if (-not [string]::IsNullOrEmpty($CompletedStep)) {
if (-not [enum]::TryParse([ValidationStep], $CompletedStep, $true, [ref]$step)) {
$validValues = ([enum]::GetNames([ValidationStep])) -join ", "
Write-Error "Error: '-CompletedStep $CompletedStep' is not a valid step. Valid values: $validValues"
exit 2
}
}
# Record progress in .azure/validate-status.json (creating it if needed).
$azureDir = Join-Path -Path $WorkspacePath -ChildPath ".azure"
if (-not (Test-Path -Path $azureDir)) {
New-Item -ItemType Directory -Path $azureDir | Out-Null
}
$validateStatusPath = Join-Path -Path $azureDir -ChildPath "validate-status.json"
$validateStatusJson = @{ completedStep = $step.ToString() } | ConvertTo-Json
[System.IO.File]::WriteAllText($validateStatusPath, $validateStatusJson + [Environment]::NewLine, (New-Object System.Text.UTF8Encoding($false)))
# Emit the next action based on the step just completed.
# Step 1: Load Plan
if ($step -eq [ValidationStep]::None) {
Write-Output 'Action: Read `.azure/deployment-plan.md` for recipe and configuration. If missing, run azure-prepare first, then come back to workflow.ps1.'
Write-Output "Next: re-run workflow.ps1 with -CompletedStep LoadPlan after completing the action."
Write-Output 'Reference: `.azure/deployment-plan.md'
exit 0
}
# Step 2: Add Validation Steps
if ($step -eq [ValidationStep]::LoadPlan) {
Write-Output 'Action: Copy the recipe''s `Validation Steps` into `.azure/deployment-plan.md` as children of `All validation checks pass`.'
Write-Output "Next: re-run workflow.ps1 with -CompletedStep AddValidationSteps after completing the action."
Write-Output 'Reference: references/recipes/README.md, `.azure/deployment-plan.md'
exit 0
}
# Step 3: Run Validation
if ($step -eq [ValidationStep]::AddValidationSteps) {
Write-Output "Action: Execute the recipe-specific validation commands."
Write-Output "Next: re-run workflow.ps1 with -CompletedStep RunValidation after completing the action."
Write-Output "Reference: references/recipes/README.md"
exit 0
}
# Step 4: Build Verification
if ($step -eq [ValidationStep]::RunValidation) {
Write-Output "Action: Build the project and fix any errors before proceeding."
Write-Output "Next: re-run workflow.ps1 with -CompletedStep BuildVerification after completing the action."
Write-Output "Reference: See the recipe for build details."
exit 0
}
# Step 5: Static Role Verification
if ($step -eq [ValidationStep]::BuildVerification) {
Write-Output "Action: Review the Bicep/Terraform for correct RBAC role assignments in code."
Write-Output "Next: re-run workflow.ps1 with -CompletedStep StaticRoleVerification after completing the action."
Write-Output "Reference: references/role-verification.md"
exit 0
}
# Step 6: Record Proof
if ($step -eq [ValidationStep]::StaticRoleVerification) {
Write-Output "Action: Populate **Section 7: Validation Proof** in the plan with the commands run and their results."
Write-Output "Next: re-run workflow.ps1 with -CompletedStep RecordProof after completing the action."
Write-Output 'Reference: `.azure/deployment-plan.md'
exit 0
}
# Step 7: Resolve Errors
if ($step -eq [ValidationStep]::RecordProof) {
Write-Output "Action: Fix any validation failures before proceeding."
Write-Output "Next: re-run workflow.ps1 with -CompletedStep ResolveErrors after completing the action."
Write-Output "Reference: See the recipe's errors.md."
exit 0
}
# Step 8: Update Status
if ($step -eq [ValidationStep]::ResolveErrors) {
Write-Output 'Action: Only after ALL checks pass, set the plan status to `Validated`.'
Write-Output "Next: re-run workflow.ps1 with -CompletedStep UpdateStatus after completing the action."
Write-Output 'Reference: `.azure/deployment-plan.md'
exit 0
}
# Step 9: Deploy (workflow complete)
if ($step -eq [ValidationStep]::UpdateStatus) {
Write-Output "Action: The azure-validate workflow is complete. If the user explicitly requested deployment, invoke azure-deploy. Otherwise STOP and report the validation results."
exit 0
}
references/scripts/workflow.sh›
#!/usr/bin/env bash
# workflow.sh
# Walks the agent through the azure-validate workflow, one step at a time.
#
# Usage:
# ./workflow.sh --workspace-path <path> [--completed-step <step>]
#
# Options:
# --workspace-path <path> Path to the workspace being validated (required).
# --completed-step <step> The workflow step the agent just completed. Omit
# on the first call to start the workflow. The
# script records the value in
# .azure/validate-status.json and returns the next
# action to take, along with the value to pass as
# --completed-step on the next call.
#
# Exit codes:
# 0 - next action emitted (or workflow complete)
# 2 - usage / argument error (missing workspace path or invalid step)
set -uo pipefail
# Valid workflow steps, in order.
VALID_STEPS=(None LoadPlan AddValidationSteps RunValidation BuildVerification \
StaticRoleVerification RecordProof ResolveErrors UpdateStatus)
# Ensure an option that consumes a value actually has one ($@ = remaining args).
need_val() {
[ "$#" -ge 2 ] || { echo "ERROR: $1 requires a value." >&2; exit 2; }
}
WORKSPACE_PATH=""
COMPLETED_STEP=""
while [ $# -gt 0 ]; do
case "$1" in
--workspace-path) need_val "$@"; WORKSPACE_PATH="$2"; shift 2 ;;
--completed-step) need_val "$@"; COMPLETED_STEP="$2"; shift 2 ;;
-h|--help)
grep '^#' "$0" | grep -v '^#!' | sed 's/^# \{0,1\}//'
exit 0 ;;
*)
echo "Unknown argument: $1" >&2
exit 2 ;;
esac
done
if [ -z "$WORKSPACE_PATH" ]; then
echo "ERROR: --workspace-path is required." >&2
exit 2
fi
if [ ! -d "$WORKSPACE_PATH" ]; then
echo "Error: --workspace-path '$WORKSPACE_PATH' does not exist or is not a directory." >&2
exit 2
fi
# Resolve the step the agent just completed (case-insensitive).
# Omitting --completed-step signals the start of the workflow (None).
STEP="None"
if [ -n "$COMPLETED_STEP" ]; then
STEP=""
for valid in "${VALID_STEPS[@]}"; do
if [ "$(printf '%s' "$COMPLETED_STEP" | tr '[:upper:]' '[:lower:]')" = \
"$(printf '%s' "$valid" | tr '[:upper:]' '[:lower:]')" ]; then
STEP="$valid"
break
fi
done
if [ -z "$STEP" ]; then
printf 'Error: '\''--completed-step %s'\'' is not a valid step. Valid values: %s\n' \
"$COMPLETED_STEP" "$(printf '%s, ' "${VALID_STEPS[@]}" | sed 's/, $//')" >&2
exit 2
fi
fi
# Record progress in .azure/validate-status.json (creating it if needed).
AZURE_DIR="$WORKSPACE_PATH/.azure"
mkdir -p "$AZURE_DIR"
VALIDATE_STATUS_PATH="$AZURE_DIR/validate-status.json"
printf '{\n "completedStep": "%s"\n}\n' "$STEP" > "$VALIDATE_STATUS_PATH"
# Emit the next action based on the step just completed.
case "$STEP" in
None)
# Step 1: Load Plan
echo "Action: Read \`.azure/deployment-plan.md\` for recipe and configuration. If missing, run azure-prepare first, then come back to workflow.sh."
echo "Next: re-run workflow.sh with --completed-step LoadPlan after completing the action."
echo "Reference: \`.azure/deployment-plan.md"
;;
LoadPlan)
# Step 2: Add Validation Steps
echo "Action: Copy the recipe's \`Validation Steps\` into \`.azure/deployment-plan.md\` as children of \`All validation checks pass\`."
echo "Next: re-run workflow.sh with --completed-step AddValidationSteps after completing the action."
echo "Reference: references/recipes/README.md, \`.azure/deployment-plan.md"
;;
AddValidationSteps)
# Step 3: Run Validation
echo "Action: Execute the recipe-specific validation commands."
echo "Next: re-run workflow.sh with --completed-step RunValidation after completing the action."
echo "Reference: references/recipes/README.md"
;;
RunValidation)
# Step 4: Build Verification
echo "Action: Build the project and fix any errors before proceeding."
echo "Next: re-run workflow.sh with --completed-step BuildVerification after completing the action."
echo "Reference: See the recipe for build details."
;;
BuildVerification)
# Step 5: Static Role Verification
echo "Action: Review the Bicep/Terraform for correct RBAC role assignments in code."
echo "Next: re-run workflow.sh with --completed-step StaticRoleVerification after completing the action."
echo "Reference: references/role-verification.md"
;;
StaticRoleVerification)
# Step 6: Record Proof
echo "Action: Populate **Section 7: Validation Proof** in the plan with the commands run and their results."
echo "Next: re-run workflow.sh with --completed-step RecordProof after completing the action."
echo "Reference: \`.azure/deployment-plan.md"
;;
RecordProof)
# Step 7: Resolve Errors
echo "Action: Fix any validation failures before proceeding."
echo "Next: re-run workflow.sh with --completed-step ResolveErrors after completing the action."
echo "Reference: See the recipe's errors.md."
;;
ResolveErrors)
# Step 8: Update Status
echo "Action: Only after ALL checks pass, set the plan status to \`Validated\`."
echo "Next: re-run workflow.sh with --completed-step UpdateStatus after completing the action."
echo "Reference: \`.azure/deployment-plan.md"
;;
UpdateStatus)
# Step 9: Deploy (workflow complete)
echo "Action: The azure-validate workflow is complete. If the user explicitly requested deployment, invoke azure-deploy. Otherwise STOP and report the validation results."
;;
esac
exit 0
SKILL.md›
---
name: azure-validate
description: "Pre-deployment validation for Azure readiness. Run deep checks on configuration, infrastructure (Bicep or Terraform), RBAC role assignments, managed identity permissions, and prerequisites before deploying. WHEN: validate my app, check deployment readiness, run preflight checks, verify configuration, check if ready to deploy, validate azure.yaml, validate Bicep, test before deploying, troubleshoot deployment errors, validate Azure Functions, validate function app, validate serverless deployment, verify RBAC roles, check role assignments, review managed identity permissions, what-if analysis, validate Container Apps deployment."
license: MIT
metadata:
author: Microsoft
version: "1.2.2"
---
# Azure Validate
> **AUTHORITATIVE GUIDANCE** — Follow these instructions exactly unless they contradict security policies given to you.
> **⛔ STOP — PREREQUISITE CHECK REQUIRED**
>
> Before proceeding, verify this prerequisite is met:
>
> **azure-prepare** was invoked and completed → `.azure/deployment-plan.md` exists with status `Approved` or later
>
> If the plan is missing, **STOP IMMEDIATELY** and invoke **azure-prepare** first.
>
> The complete workflow ensures success:
>
> `azure-prepare` → `azure-validate` → `azure-deploy`
## Triggers
- Check if app is ready to deploy
- Validate azure.yaml or Bicep
- Run preflight checks
- Troubleshoot deployment errors
## Rules
1. Run after azure-prepare, before azure-deploy
2. All checks must pass—do not deploy with failures
3. ⛔ **Destructive actions require `ask_user`** — [global-rules](references/global-rules.md)
## Steps
Run the workflow script and follow its instructions. It walks you through each validation step one at a time, recording progress in `.azure/validate-status.json`. Use [references/scripts/workflow.ps1](references/scripts/workflow.ps1) on Windows or [references/scripts/workflow.sh](references/scripts/workflow.sh) on macOS/Linux.
Start by calling the script **without** the completed-step argument:
```bash
pwsh references/scripts/workflow.ps1 -WorkspacePath <workspace-path>
# macOS/Linux: bash references/scripts/workflow.sh --workspace-path <workspace-path>
```
Each run prints the next action and the value to pass next. Perform the action, then re-run with that value (`-CompletedStep <value>` for pwsh, `--completed-step <value>` for bash). Repeat until it reports the azure-validate workflow is complete.
The steps reference recipe details in [references/recipes/README.md](references/recipes/README.md) and role checks in [references/role-verification.md](references/role-verification.md).
> **⛔ VALIDATION AUTHORITY**
>
> This skill is the officially verified way to set plan status to `Validated`. You MUST follow the script's instructions to completion before setting status to `Validated`.
> Do NOT set status to `Validated` without doing so.
---
> **⚠️ NEXT STEP — DEPENDS ON USER INTENT**
>
> After ALL validations pass, check whether the user asked to deploy:
> - **If the user explicitly requested deployment**, you **MUST** invoke **azure-deploy** to execute it. Do NOT run `azd up`, `azd deploy`, or any deployment commands directly — let azure-deploy handle execution.
> - **If the user only asked to validate or prepare** (not deploy), STOP after recording proof and setting status to `Validated`. Report the validation results and do NOT invoke azure-deploy.
>
> If any validation failed, fix the issues and re-run azure-validate before proceeding.