Installation
gh skills-hub install azure-app-onboard Don't have the extension? Run gh extension install samueltauil/skills-hub first.
Download and extract to your repository:
.github/skills/azure-app-onboard/ Extract the ZIP to .github/skills/ in your repo. The folder name must match azure-app-onboard for Copilot to auto-discover it.
Skill Files (68)
SKILL.md 6.3 KB
---
name: azure-app-onboard
description: "End-to-end orchestrator: from a business idea, app idea, or existing app to running Azure deployment with cost estimates and pre-deploy approval. Analyzes your app, auto-detects the right Azure services, scaffolds infrastructure code, and deploys β tailored to your app, not a template. Handles moving existing apps to Azure without rewriting or with minimal changes. WHEN: bring your app to Azure, plan my app, cost to run, is my code ready to deploy, deploy my app to the cloud, deploy all my services, what Azure services do I need, plan my Azure deployment, deploy my new app to Azure, one-click deploy, I have an app and want it on Azure, migrate my app to Azure, help me get started, build an app, no code yet, starter project. DO NOT USE FOR: use azd for deployment(use azure-deploy), optimizing existing costs (use azure-cost), code readiness checks only (use azure-app-onboard-prereq)."
license: MIT
metadata:
author: Microsoft
version: "1.2.3"
---
# Azure App Onboard
> β **Every repo goes through the full pipeline (Steps 1β10). No exceptions.** Do not skip steps, refuse, or short-circuit based on what you recognize. Follow the Workflow table below sequentially β read each step's references before acting.
## Quick Reference
| Property | Value |
|----------|-------|
| Best for | Developers who know what to build but not which Azure services to use |
| Inputs | Business idea or existing codebase, budget/scale preferences (optional) |
| Outputs | Architecture plan, cost estimate, IaC files, deployed Azure resources |
| Phases | Discover β Architect β Scaffold β Deploy (self-contained, no external skill calls) |
## When to Use This Skill
- Deploy existing code without knowing which Azure services to use
- Check if your existing code is ready to deploy to Azure
- Move an existing app to Azure without rewriting or with minimal changes
- Get cost estimates before committing to infrastructure
- Understand architecture decisions and rejected alternatives
- Get answers to Azure architecture or service selection questions (e.g., "What database should I use?")
- Get guided Azure onboarding without prior experience
## When NOT to Use
| Scenario | Use Instead |
|----------|-------------|
| Run `azd up` or execute an existing deployment | `azure-deploy` |
| Optimize existing Azure spend | `azure-cost` |
| Generate Bicep/Terraform for a known architecture | `azure-prepare` |
| Validate infrastructure or run preflight checks | `azure-validate` |
| Troubleshoot a running Azure deployment | `azure-diagnostics` |
| Deploy to or manage AKS/Kubernetes directly | `azure-kubernetes` |
| Look up or list existing Azure resources | `azure-resource-lookup` |
## Pipeline Rules
> β **You MUST read [`references/pipeline-rules.md`](references/pipeline-rules.md) at the start of every AppOnboard session.** It contains approval gates, phase lifecycle, session artifacts, deploy-as-is, and security baseline rules.
## Workflow
> β **Deploy recovery:** After deploy gate approval OR before any `az deployment`/`az webapp deploy`/`az acr build` β if you haven't read `deploy/SKILL.md`, read `.copilot-azure/sessions/{id}/deploy-checklist.md` first, then `deploy/SKILL.md`. β NEVER invoke `{"skill": "azure-deploy"}` β that is a DIFFERENT skill for a DIFFERENT workflow.
> β **Post-scaffold transition (MANDATORY):** Immediately after `scaffold-manifest.json` is written, YOUR NEXT ACTION MUST be Step 8 (Deploy Approval Gate) β NOT a summary report, NOT a "here are the generated files" message, NOT a completion signal. Confirm `context.json` has `completedPhases: [...,"scaffold"]` + `currentPhase: "deploy"` (update it yourself if the scaffold subagent didn't). Re-read [approval-gates.md Β§ Deploy Gate](references/approval-gates.md) if evicted from context (scaffold reference loading is heavy), then present the exact prompt: **"π Ready to deploy? (Yes / Run manually / Edit plan / Cancel)"**. This gate is the LAST content in your response β wait for the user's reply.
| # | Step | Action | Reference |
|---|------|--------|-----------|
| 1 | **Session check + Azure login** | Create/resume session, verify Azure CLI auth, resolve subscription + user identity | β **You MUST read [session-protocol.md](references/session-protocol.md)** |
| 2 | **Scope triage** | Check azd markers, triage question. Empty workspace or code-only (no infra) β Step 3 directly. | β Read [intent-gathering.md](references/intent-gathering.md) Β§ Scope Triage |
| 3 | **Prereq scan** | β Skip if `completedPhases` includes `"prereq"`. Otherwise: invoke `{"skill": "azure-app-onboard-prereq"}`. Write `prereq-output.json`, update `context.json`. **Halt if:** `overallHealth: "blocked"` OR `routeToSkill` set. | |
| 4 | **Gather intent** | Present prereq results, confirm stack + Azure services, ask remaining questions. | β Read [intent-gathering.md](references/intent-gathering.md) Β§ After Prereq Returns |
| 5 | **Plan architecture** | Write `prepare-plan.json`. | β **You MUST read [prepare/SKILL.md](prepare/SKILL.md)** |
| 6 | **Scaffold approval gate** | Display plan for user approval BEFORE generating any files. | β Read [approval-gates.md](references/approval-gates.md) Β§ Scaffold Gate |
| 7 | **Scaffold** | Generate IaC, self-review. Write `scaffold-manifest.json`. Update `context.json`. | β **You MUST read [scaffold/SKILL.md](scaffold/SKILL.md)** |
| 8 | **Deploy approval gate** | Display validation summary. β After approval: FIRST read deploy-checklist.md β deploy/SKILL.md. NEVER `{"skill": "azure-deploy"}`. | β Read [approval-gates.md](references/approval-gates.md) Β§ Deploy Gate |
| 9 | **Deploy** | Execute IaC, health-check. Write `deploy-result.json`. | β **You MUST read [deploy/SKILL.md](deploy/SKILL.md)** |
| 10 | **Handoff** | Surface deployment identity, cleanup commands, next steps. | β **You MUST read [`handoff-protocol.md`](references/handoff-protocol.md)** |
## Error Handling
| Error | Remediation |
|-------|-------------|
| Phase fails | Halt, report phase + error. User decides: retry, skip, abort. |
| MCP server unavailable | Skip affected checks, add disclaimer to `costEstimate.assumptions[]` and every approval gate. |
| Missing RBAC | Report required role + `az role assignment` command. |
> **Shared references:** [MCP tools](references/mcp-tool-reference.md) (cross-phase tool parameters) | [IaC resources](references/iac-resources.md) (Azure resource docs for troubleshooting) SKILL.md 7.1 KB
# Deploy β IaC Execution & Health Verification
## Quick Reference
| Property | Value |
|----------|-------|
| Best for | Executing validated IaC against Azure, health-checking deployed resources |
| Inputs | `prepare-plan.json` + `scaffold-manifest.json` from `.copilot-azure/sessions/{id}/` |
| Outputs | `deploy-result.json` written to session directory |
| Parent | [azure-app-onboard](../SKILL.md) |
## When to Use This Skill
Invoked by the `azure-app-onboard` orchestrator at Phase 4 when `scaffold-manifest.json` exists with `files[]` and `validationResult`. Not directly user-routable.
> **Return to orchestrator:** When complete, return control to `azure-app-onboard` for handoff (Step 10). Do NOT start new phases.
## When NOT to Use
| Scenario | Use Instead |
|----------|-------------|
| Plan architecture, map services, estimate costs | [prepare](../prepare/SKILL.md) |
| Generate IaC files from a plan | `azure-app-onboard` Step 7 (scaffold) |
| Run `azd up` or execute existing deployment templates | `azure-deploy` |
| Debug a running app after deployment | `azure-diagnostics` |
| Optimize existing Azure spending | `azure-cost` |
## Workflow
> β **Sub-agent delegation is MANDATORY for Step 0.** Read `subagent-preflight.md`, then dispatch as a `task` with the **COMPLETE and UNMODIFIED** template text between `<<<TEMPLATE_START>>>` / `<<<TEMPLATE_END>>>` delimiters. Do NOT summarize or rewrite the template β the sub-agent needs every "Read [file]" instruction to produce a correct `deploy-checklist.md`. Append session artifact data AFTER the template block. If your next action after reading the template is anything other than `task`, you are executing it inline instead of delegating.
> β **Healing loop:** ask user after 3 attempts, then every 5 (counter = `healingAttempts[].length`).
> β **Region lock:** Before `az deployment` retry, compare `--location` against `prepare-plan.json.deploymentVariables.location`. If changed β re-approval gate required. Update plan after approval.
> β **After compaction or any `az deployment`/`az webapp deploy`/`az acr build`/failed health check: re-read `deploy-checklist.md`.** If missing β fill from [`deploy-checklist-template.md`](references/deploy-checklist-template.md). On significant context loss: also re-read this SKILL.md.
| # | Step | Action | Artifact | Reference |
|---|------|--------|----------|-----------|
| 0 | **Dispatch preflight sub-agent** | β **You MUST dispatch [`subagent-preflight.md`](references/subagent-preflight.md) as a `task`.** β agent_type: `"task"` β NEVER `"general-purpose"`. Read the template, then your NEXT action MUST be `task`. If after reading the template your next action is `powershell`, `view`, or anything other than `task`, STOP β you are executing inline instead of delegating. Writes `deploy-checklist.md`. **`view` it immediately after return.** | `deploy-checklist.md` | β **You MUST read [`subagent-preflight.md`](references/subagent-preflight.md)** |
| 1 | **Read upstream artifacts** | Load `prepare-plan.json` + `scaffold-manifest.json`. Check `validationResult`. Resolve subscription + deployment variables. | β | β |
| 3 | **Preflight checks** | Auth, **mandatory what-if preview**, RBAC, RG per `deploy-checklist.md` Β§ Preflight. | β | β **You MUST read `deploy-checklist.md`** (re-read if compaction occurred) |
| 4 | **Deploy approval gate** | Present cost + resource summary per `deploy-checklist.md` Β§ Deploy approval gate format. | β | β |
| 5b | **Write deploy-result.json skeleton** | β Read [`deploy-schemas.ts`](references/deploy-schemas.ts), write skeleton (`status: "in-progress"`). Must exist BEFORE first `az` command. | `deploy-result.json` | β **You MUST read [`deploy-schemas.ts`](references/deploy-schemas.ts)** |
| 6 | **Execute deployment** | β **BEFORE `az deployment sub create`:** Generate portal link β `$dn="{deploymentName}"; $r="/subscriptions/{subId}/providers/Microsoft.Resources/deployments/$dn"; $l="https://portal.azure.com/#view/Microsoft_Azure_Resources/DeploymentDetails.MenuView/~/overview/id/$($r.Replace('/','%2F'))"; Write-Output "LINK=$l"`. β **Auto-open link in browser:** `Start-Process $l 2>$null`. Print bare URL in chat (ctrl-clickable).<br>Auto-generate ALL `@secure()` params (`openssl rand -base64 32 \| tr -d '/+='`), NEVER `ask_user` for passwords; on retry reuse from `deploy-secrets.env` or Key Vault β NEVER regenerate (see deploy-safety.md Β§ Deploy Checklist). THEN deploy IaC. | β | β **You MUST read `deploy-checklist.md`** Β§ Execute deployment |
| 6b | **Deploy application code** | β Deploy code for EVERY service in `prepare-plan.json.services[]`. Follow `deploy-checklist.md` Β§ Code deploy. | β | β **You MUST read `deploy-checklist.md`** Β§ Code deploy |
| 7 | **Health-check + SCM re-disable** | HTTP GET per endpoint (max 3 iterations). β **Multi-service apps:** Also inspect the response body for error patterns (`connection refused`, `MODULE_NOT_FOUND`, `localhost`, `SET-IN-DEPLOY-PHASE`) β HTTP 200 alone does not mean functional when the app depends on another service or KV secrets. Then β for EVERY App Service/Functions app run BOTH commands β no exceptions: `az rest --method put --url "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/{app}/basicPublishingCredentialsPolicies/scm?api-version=2023-12-01" --headers "Content-Type=application/json" --body '{"properties":{"allow":false}}'` then verify: `az rest --method get --url "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/{app}/basicPublishingCredentialsPolicies/scm?api-version=2023-12-01" --query properties.allow -o tsv` (must return `false`). | `deploy-result.json` full | β **You MUST read `deploy-checklist.md`** Β§ Health check |
| 8 | **Finalize artifacts** | β Read [`deploy-schemas.ts`](references/deploy-schemas.ts). β Re-read `deploy-checklist.md` Β§ Artifact verification β follow ALL 5 checks. β **No "live"/handoff message until you overwrite the skeleton `deploy-result.json`** β flip `status` off `"in-progress"` (β `succeeded`/`failed`) and fill healthStatus, endpoints, completedUtc, deploymentNames, healingAttempts. Write `deployment-summary.md` (status table + health + portal link(s) + cleanup commands β same content as your handoff message). Update `context.json` β add `"deploy"` to `completedPhases`, `currentPhase: null`, `lastModifiedUtc`. Read back to confirm `status != "in-progress"` and `"deploy"` β `completedPhases`. β **Then STOP β return to orchestrator. No further CLI commands.** | `deploy-result.json` final + `deployment-summary.md` + `context.json` update | β **You MUST read [`deploy-schemas.ts`](references/deploy-schemas.ts)** + β **Re-read `deploy-checklist.md` Β§ Artifact verification** |
| 9 | **Error handling + healing** | β **Only if Steps 6/6b/7 returned nonzero exit code or health check failed.** Skip entirely on clean deploys. Classify errors, healing loop, PLAN_LEVEL_CHANGE re-approval per `deploy-checklist.md` Β§ During healing. β **Even on unrecoverable failure:** write `deploy-result.json` with `status: "failed"` and `errorDetails` before returning to orchestrator β the artifact must always exist. | β | β **You MUST read [`error-classification.md`](references/error-classification.md)** |
approval-gate-template.md 2.9 KB
# Approval Gate Template
Display template for the deploy approval gate. Present after preflight validation, before execution.
## Display Format
```
## Deploy Approval
π’ **Subscription:** {context.json.azure.subscriptionName} (`{context.json.azure.subscriptionId}`)
π **Resource Group:** {context.json.azure.resourceGroup}
π **Region:** {context.json.azure.region}
**Services:**
| Service | SKU | Region | Resource Name |
|---------|-----|--------|---------------|
{for each service in prepare-plan.json.services[]}
**π° Estimated Monthly Cost:** ${costEstimate.monthlyUsd}/month
| Service | SKU | Monthly |
|---------|-----|---------|
{for each item in costEstimate.breakdown[]}
{costEstimate.disclaimer}
**π Validation:** {scaffold-manifest.json.validationResult.status}
{if FLAGGED findings from scaffold self-review β β οΈ list each}
**π What-if preview:** {N} resources to create, {N} to modify, {N} unchanged
{if any Delete operations β β οΈ list each deleted resource with name + type}
**Generated Files:**
{for each file in scaffold-manifest.json.files[]}
**π¦ Deployment Summary:**
{Display the "What's Being Deployed" table here, built from prepare-plan.json.services[] β showing which components map to which Azure services, SKUs, and estimated costs. (deployment-summary.md is not written until the deploy/handoff phase)}
---
**π Ready to deploy? (Yes / Run manually / Edit plan / Cancel)**
```
## Response Handlers
| Response | Action |
|----------|--------|
| **Yes** | Execute deployment (Step 6 of deploy workflow). Do NOT re-confirm. |
| **Run manually** | Show exact CLI commands based on `iacFormat`:<br>**Bicep (subscription-scope, default):** `az deployment sub create --subscription {subscriptionId} --location {location} --template-file infra/main.bicep --parameters @infra/main.parameters.json --query properties.provisioningState -o tsv`<br>**Bicep (resource-group scope, after 403 fallback):** `az deployment group create --resource-group {rg} --template-file infra/main.bicep --parameters @infra/main.parameters.json --query properties.provisioningState -o tsv`<br>**Terraform (alternative):** `cd infra && terraform init && terraform plan -out=tfplan && terraform apply tfplan`. Stop. |
| **Edit plan** | Ask what to change β write to `context.json.overrides[]` β re-run prepare (Step 4) β re-scaffold β return to approval gate |
| **Cancel** | Preserve all session artifacts. Say "πΎ Session preserved β resume anytime." Stop. |
## Rules
- β Gate is the LAST content in the response β no continued execution until user replies
- β SKU column must show exact Azure SKU code + tier name (e.g., "F1 (Free)", "B1 Linux (Basic)") β not generic labels like "Free tier"
- Always show cost even if $0 (free tier) β user needs confirmation
- Surface ALL `FLAGGED` self-review findings β user must see risks before approving
- If validation failed, show failures and block Yes option until resolved
blocked-patterns.md 5.6 KB
# Blocked Patterns
Commands the agent must NEVER execute. Block decisions are non-negotiable β user must run blocked commands manually outside AppOnboard.
| Pattern | Action | Reason |
|---------|--------|--------|
| `rm -rf` (any path outside a fresh temp dir) | β Block | Prevents accidental deletion of IaC, app code, or session artifacts β especially `infra/`, `.azure/`, `.copilot-azure/`. |
| `git reset --hard`, `git checkout -- <path>`, `git restore`, `git clean` | β Block | Discards uncommitted work. During region-fallback healing the agent edits Bicep/app config; these wipe the user's unstaged changes irrecoverably. |
| `git push --force` / `--force-with-lease` (any branch) | β Block | Prevents force-push of generated code over remote history. |
| `--no-verify` (on `git commit` / `git push`) | β Block | Bypasses hooks (secret-scan, lint) that guard the commit. |
| `DROP TABLE` / `DROP DATABASE` | β Block | Prevents data loss |
| `terraform destroy` | β Block | Prevents accidental teardown (user must run manually) |
| `az group delete` | β HARD BLOCK | **NEVER delete resource groups yourself.** During healing: if switching regions/RGs, add the old RG to your `orphanedResourceGroups[]` list (per `OrphanResourceGroup` in [`deploy-schemas.ts`](deploy-schemas.ts)) instead of deleting it. At handoff: emit `az group delete` commands in the handoff message for the USER to run β the agent never executes them. If you are about to type `az group delete` into a terminal command, STOP β you are violating this rule. Track it in `orphanedResourceGroups[]` instead. |
| `az containerapp up --source` / `az containerapp create` | β Block | Creates ACR + CA Environment + Log Analytics imperatively β orphan resources invisible to `terraform destroy`, `az deployment sub delete`, and session tag-based bulk cleanup. State drift from IaC is unrecoverable. The Container App MUST be created via Bicep `az deployment sub create` β for code deploy on an existing CA use `az containerapp update --source` (Step 6d) |
| `az appservice plan update` | β Block | Imperative SKU change β edit Bicep + redeploy |
| `az webapp update` | β Block | Imperative resource modification β all changes via IaC |
| `az functionapp update` | β Block | Imperative resource modification β all changes via IaC |
| `az webapp deployment source config-zip` | β Block | Requires SCM basic auth β use `az webapp deploy` (Entra auth) |
| `az webapp deploy --track-status` | β Block | `--track-status` flag does not exist. Remove it. |
| `az webapp up` / `az webapp create` / `az appservice plan create` | β Block | Creates App Service Plan + App imperatively β bypasses IaC entirely |
| `az containerapp update` (config changes) | β Block | Imperative resource modification β all changes via IaC |
| `az containerapp update --revision-suffix` (no config changes) | β οΈ ALLOWED | KV secret rotation only β when KV secrets were updated post-deploy and a new revision is needed to pick up cached values |
| `az webapp delete` | β Block | Imperative resource deletion β destroys resources outside IaC |
| `az appservice plan delete` | β Block | Imperative plan deletion β remove from Bicep + redeploy instead |
| `az containerapp update --image` | β Block (during healing) | Imperative image swap causes IaC drift β update Bicep + redeploy |
| Inline secret values in CLI args | β Block | `--parameters password=MyP@ss$word!` breaks shell escaping and leaks secrets in terminal history. Pass secrets via `main.parameters.json`, `terraform.tfvars`, or `az keyvault secret set --file`. |
| Writing secrets to temp files on disk | β Block | β NEVER write secrets to temp files on disk. Seed secrets into Key Vault via `az keyvault secret set`, then reference via SecretUri in IaC. Temp files risk exposure in crash dumps, logs, and unprotected storage. |
| `az group create` (during healing) | β HARD BLOCK β **one sanctioned exception** | **NEVER create resource groups imperatively during healing.** All RG creation must go through `az deployment sub create` with Bicep `targetScope = 'subscription'`. If you need a new RG for region fallback, update the Bicep region parameter and redeploy. **Sole exception:** the documented 403 scope-fallback in [`deploy-safety.md`](deploy-safety.md) Β§ 403 Scope Fallback β when `az deployment sub create` returns 403, rescoping Bicep to RG-scope requires `az group create` (with all 5 AppOnboard tags). That path is allowed. |
| `az rest --method put/patch` (for individual resource creation) | β HARD BLOCK | **NEVER create individual Azure resources via REST API as a fallback for Bicep failures.** After a deployment failure, the ONLY allowed remediation is: fix the Bicep parameters/template β re-run `az deployment sub create`. Compiling BicepβARM and deploying via REST is still imperative resource creation. |
| Disabling a security control to unblock β `require_secure_transport`/TLS β OFF, HTTPS-only off, KV purge protection off, auth off (via `az ... parameter set` OR editing the Bicep) | β HARD BLOCK | **NEVER weaken a security control to make a failing deploy pass.** A DB TLS handshake failure means the *client* lacks SSL config β fix the client (prereq `W-MYSQL-SSL`/`W-PG-SSL`) or surface the tradeoff to the user. Downgrading the server control is forbidden. |
| `Compress-Archive -Path $files.FullName` | β Block | Absolute paths flatten directory structure β app crashes on `./src/app` not found. Use `System.IO.Compression.ZipFile` with relative paths from workspace root. On Windows, normalize: `$entryName = $relativePath.Replace('\', '/')`. |
> **Repos with existing `azure.yaml`:** See [`pipeline-rules.md`](../../references/pipeline-rules.md) Β§ azure.yaml prohibition. Deploy via `az deployment sub create` β do NOT run `azd up`.
code-deployment-appservice.md 4.6 KB
# Code Deployment β App Service & Functions
After IaC deployment creates the Azure resources, deploy application code.
> β **`--subscription {subscriptionId}` on EVERY `az` command.**
> β **Verify `SCM_DO_BUILD_DURING_DEPLOYMENT=true` is active BEFORE deploying.** ARM timing can delay propagation. Check: `az webapp config appsettings list -g {rg} -n {app} --query "[?name=='SCM_DO_BUILD_DURING_DEPLOYMENT'].value" -o tsv`. If not `true`: `az webapp config appsettings set -g {rg} -n {app} --settings SCM_DO_BUILD_DURING_DEPLOYMENT=true ENABLE_ORYX_BUILD=true`. Wait 10s. If `az webapp deploy` reports "Build successful. Time: 0(s)", Oryx was skipped β use Kudu zipdeploy instead.
> β **`ORYX_DISABLE_COMPRESSION=true`** and **`WEBSITES_CONTAINER_START_TIME_LIMIT=1800`** must be in Bicep app settings (from `prepare-plan.json.deployStrategy.requiredAppSettings`).
## Pre-Deploy Verification (Step 6a)
> β **TypeScript projects:** Oryx with `NODE_ENV=production` skips devDependencies. If `typescript`, `@types/*`, or build tools are in `devDependencies`, move them to `dependencies` before zipdeploy. Alternative: set `NPM_CONFIG_PRODUCTION=false` as app setting so Oryx installs devDeps during build.
> β **Wait for App Service to stabilize** (F1: 30-120s cold start). Poll `az webapp show -g {rg} -n {app} --query state` every 10s, max 2 min. If not `Running`, check logs.
When `deployStrategy.codeDeployPattern == "startup-install"`, surface: "β οΈ First cold start: 2-5 min (native module compilation)."
## Zip Deploy (Step 6b)
SCM lifecycle: enable β deploy β re-disable.
**Enable SCM:**
```powershell
az rest --method put --url "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/{app}/basicPublishingCredentialsPolicies/scm?api-version=2023-12-01" --headers "Content-Type=application/json" --body '{"properties":{"allow":true}}'
```
**Choose deploy method:**
| Runtime | Needs Oryx? | Method |
|---|---|---|
| Python, Node.js, Ruby, PHP | Yes | **Kudu zipdeploy** (`/api/zipdeploy`) |
| .NET, Java, static front-end | No | `az webapp deploy --type zip` |
β **OneDeploy NEVER triggers Oryx** β use Kudu zipdeploy for runtimes needing server-side install.
β **NEVER use `az webapp deployment source config-zip`** β deprecated.
β **`az webapp deploy` does NOT support `--track-status`.**
### Kudu Zipdeploy (Oryx-Dependent Runtimes)
For apps needing server-side package installation (Python, Node.js, Ruby, PHP), use Kudu zipdeploy directly:
```powershell
# Get publishing credentials
$creds = az webapp deployment list-publishing-credentials --subscription {sub} -g {rg} -n {app} --query "{user:publishingUserName, pass:publishingPassword}" -o json | ConvertFrom-Json
$auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$($creds.user):$($creds.pass)"))
# Deploy via Kudu zipdeploy (triggers Oryx pip install)
Invoke-WebRequest -Uri "https://{app}.scm.azurewebsites.net/api/zipdeploy?isAsync=true" -Method POST -InFile $zipPath -Headers @{Authorization="Basic $auth"} -ContentType "application/zip" -UseBasicParsing
# Poll until build completes
for ($i = 1; $i -le 40; $i++) {
Start-Sleep -Seconds 15
$resp = Invoke-WebRequest -Uri "https://{app}.scm.azurewebsites.net/api/deployments/latest" -Headers @{Authorization="Basic $auth"} -UseBasicParsing
$deploy = $resp.Content | ConvertFrom-Json
if ($deploy.complete -eq $true) { break }
}
```
Prerequisites:
- `SCM_DO_BUILD_DURING_DEPLOYMENT=true` must be set (verified in Step 6a)
- Runtime manifest must be at the zip root β Oryx detects the runtime from it:
- Python: `requirements.txt` (or `pyproject.toml`)
- Node.js: `package.json` (+ lockfile)
- Ruby: `Gemfile`
- PHP: `composer.json`
- Do NOT pre-install packages locally β let Oryx run the install remotely
> **SCM auth lifecycle (REST API toggle β no Bicep edits):**
>
> IaC has `scm.allow: true` (deploy convenience). Deploy phase:
> Deploy code β health check β re-disable via REST API β verify `false`.
> If re-disable fails, log but don't block β add postDeployRecommendation.
**Zip creation:** Use `System.IO.Compression.ZipFile` with relative paths from workspace root. **On Windows, normalize entry paths: `$entryName = $relativePath.Replace('\', '/')` β ZipFile preserves backslashes which Linux App Service cannot resolve.** Never use `Compress-Archive -Path $files.FullName` β absolute paths flatten the directory structure, causing app crashes (`./src/app` not found).
## Database Post-Deploy Verification
> β **You MUST read [`database-post-deploy.md`](database-post-deploy.md)** for migration discovery, execution via App Service SSH, error handling, and PostgreSQL-specific checks.
code-deployment-container-apps.md 8.4 KB
# Code Deployment β Container Apps
After IaC deployment creates the Container App with a placeholder image (`mcr.microsoft.com/azuredocs/containerapps-helloworld:latest`), deploy the actual application code. This is **Phase 2** of the two-phase Container Apps deployment pattern.
> β **`--subscription {subscriptionId}` on EVERY `az` command** (from `context.json.azure.subscriptionId`). Without it, the CLI uses whatever subscription is currently active β which may have changed since the prepare phase. This applies to ALL commands below.
> β **Phase 2 is NOT optional.** If IaC deployed a Container App with a placeholder image, you MUST execute Phase 2 before health checks. Do NOT leave a placeholder image and tell the user to deploy code manually.
**Determine the code deploy path:**
| Condition | Path | Steps |
|-----------|------|-------|
| ACR exists in IaC (`services[]` has Container Registry) | ACR build + image update | Steps 1β4 below |
| No ACR in IaC, simple app (single Dockerfile or source) | Add ACR to IaC + redeploy, then ACR build | Step 0 + Steps 1β4 |
| No ACR in IaC, no Dockerfile, Oryx-compatible (Node/Python/Go/.NET) | `az containerapp update --source .` | Step 5 (Oryx shortcut) |
## Step 0 β Add ACR to IaC (if not already present)
Add `container-registry.bicep` module (ACR Basic, `adminUserEnabled: false`) + AcrPull role assignment for CA's managed identity (role GUID: `7f951dda-4ed3-4680-a7ca-43fe172d538d`). Redeploy IaC, wait ~60s for AcrPull role propagation. Log as healing attempt.
β Update `prepare-plan.json` (add ACR to `services[]`), `scaffold-manifest.json.files[]`, and `costEstimate`.
## Steps 1β4 β ACR Build + Image Update (IaC-compliant)
| Step | Command | Notes |
|------|---------|-------|
| 1. Build image | `az acr build --subscription {subscriptionId} -r {acrName} -t {appName}:latest . --no-logs` | Builds from workspace root. If `buildRequirements.hasBuildKitSyntax`, use `-f Dockerfile.azure` (see BuildKit handling below). β **Build-time env vars:** If Dockerfile has `ARG NEXT_PUBLIC_*` or `ARG VITE_*`, pass `--build-arg NEXT_PUBLIC_API_URL=https://{api-fqdn}` to inject the deployed API URL. These vars are baked into the JS bundle at build time β runtime env vars have no effect on client-side code. Get the API FQDN from Phase 1 output: `az containerapp show -g {rg} -n {apiApp} --query properties.configuration.ingress.fqdn -o tsv`. |
| 2. Update Bicep image | Edit `infra/modules/{containerapp}.bicep`: replace placeholder with `image: '{acrLoginServer}/{appName}:latest'`. Add ACR registry config: `registries: [{ server: acrLoginServer, identity: 'system' }]` | IaC-only β no imperative `az containerapp update` |
| 3. Redeploy | `az deployment sub create --subscription {subscriptionId} --location {location} --template-file infra/main.bicep --parameters @infra/main.parameters.json --parameters administratorLoginPassword=$dbPassword --name app-onboard-code-deploy-{timestamp} --query properties.provisioningState -o tsv` | Emit new portal link. β `$dbPassword` = existing KV secret, never new/placeholder β see [Parameter Pass-Through](#parameter-pass-through-bicep-redeploy-safety). |
| 4. Verify revision | `az containerapp show --subscription {subscriptionId} -g {rg} -n {ca} --query "{revision:properties.latestReadyRevisionName, image:properties.template.containers[0].image}" -o json` | Confirm image is not the placeholder |
> β **`az acr build` failures count toward the `deploy-result.json.healingAttempts[]` counter.** After 3 failed builds (even with different root causes), pause and present a diagnosis to the user: "Web image build failed 3 times: [root causes]. Continue healing? (Yes / Cancel)." Each build fix attempt = 1 healing entry. Cross-reference deploy SKILL.md healing loop rule.
> β **Windows: `az acr build` may fail with `UnicodeEncodeError`.** Azure CLI log streaming crashes on non-ASCII characters (β, β) using Windows `charmap` codec. Append `--no-logs` to the build command. Build success/failure is still reported via exit code.
## Step 5 β Oryx Shortcut (no ACR needed, no Dockerfile)
```powershell
az containerapp update --subscription {subscriptionId} -g {rg} -n {ca} --source . --set-env-vars NODE_ENV=production
```
> β **`az containerapp update --source` is allowed for code deploy ONLY.** This is different from `az containerapp up --source` (which is β BLOCKED because it creates resources imperatively). `update --source` updates an EXISTING Container App β no state drift.
## Seed KV Secrets (between Phase 1 and Phase 2)
After Phase 1, seed real secret values into KV before Phase 2 activates `secretRef`:
```powershell
az keyvault secret set --subscription {subscriptionId} --vault-name {kvName} --name {secret-name} --value $generatedValue
```
> β Use the SAME generated password passed to `az deployment sub create`. Shell variables don't persist between tool calls β reload from `deploy-secrets.env` (see deploy-safety.md Β§ Deploy Checklist) or pass to BOTH commands in the same block.
## After Code Deploy (all paths)
- Wait 30β60s for the new revision to become ready
- Proceed to Step 7 (Health-Check Endpoints)
- If health check returns placeholder content β image update didn't take effect. Check `az containerapp revision list`
> β **`az containerapp revision restart` does NOT re-resolve KV secrets.** Container Apps caches KV-backed `secretRef` values at revision *creation* time. To pick up updated KV secrets, create a NEW revision by redeploying Bicep (preferred) or `az containerapp update --revision-suffix rev{timestamp}`. Do NOT use `revision restart` for KV secret rotation β it only restarts the container with the same cached values.
> β **KV secretRef AND ACR registries require managed identity + roles to exist first.** Phase 1 of the two-phase deploy creates the Container App with a placeholder image, `registries: []`, and `secrets: []`. Both KV `secretRef` and ACR `registries` with `identity: 'system'` fail in Phase 1 because the CA's managed identity doesn't exist yet (no principalId β AcrPull/KV role assignment fails β "Operation expired"). After Phase 1 completes and RBAC propagates (~60s), Phase 2 redeploys with ACR registries + KV secretRef + real image + correct `targetPort`.
## Config-File Apps (Go/Viper, Spring Boot, etc.)
Creating Azure-specific config (e.g., `config-azure.yml`) is fine for non-secret values.
> β **NEVER bake secrets into config files COPY'd into Docker images.**
**Secret injection:** Config uses placeholders β `az containerapp secret set` β `az containerapp update --set-env-vars KEY=secretref:name` β framework env var override.
> **Go/Viper:** `AutomaticEnv()` maps `POSTGRES_PASSWORD` β `postgres_password` (underscores), NOT `postgres.password`. Without `SetEnvKeyReplacer(strings.NewReplacer(".", "_"))`, env vars can't override nested keys.
## BuildKit Dockerfile Handling
ACR's `az acr build` uses the classic Docker builder β it does NOT support BuildKit. When `buildRequirements.hasBuildKitSyntax == true`: build using the ACR-compatible copy instead: `az acr build -f Dockerfile.azure .`. The user's original Dockerfile stays untouched. If `Dockerfile.azure` doesn't exist yet, create it by stripping BuildKit syntax from the original Dockerfile per [`dockerfile-generation.md Β§ ACR Build Compatibility`](../../scaffold/references/dockerfile-generation.md).
## Parameter Pass-Through (Bicep Redeploy Safety)
β **On every redeploy, pass the SAME values you originally used** β a full desired-state apply, not a patch, so an omitted param reverts to its default and a regenerated secret overwrites the live value. Two params bite:
- **`containerImage`** β omitting it reverts the Container App to the placeholder image.
- **`administratorLoginPassword`** (DB modules) β passing a new or placeholder value silently RESETS the database admin password, desyncing it from the Key Vault secret the app reads β runtime auth failures (`Access denied for user`).
```powershell
$dbPassword = az keyvault secret show --subscription {subscriptionId} --vault-name {kvName} --name {db-secret} --query value -o tsv
az deployment sub create ... `
--parameters containerImage='{acrLoginServer}/{appName}:latest' `
--parameters administratorLoginPassword=$dbPassword `
--query properties.provisioningState -o tsv
```
## Database Post-Deploy Verification
> β **You MUST read [`database-post-deploy.md`](database-post-deploy.md)** for migration discovery, execution via Container Apps exec, error handling, and PostgreSQL-specific checks.
code-deployment-swa.md 5.6 KB
# Code Deployment β Static Web Apps
## Static Web Apps β Content Deployment (Step 6c)
Deploy content using the SWA CLI with deployment token.
**Pre-check:** Verify `swa` CLI is installed: `npx --yes @azure/static-web-apps-cli --version`. If not available, install: `npm install -g @azure/static-web-apps-cli`.
> β **Pre-deploy: Build the frontend if SPA source (not pre-built HTML).** Check if the SWA component directory has a `package.json` (or equivalent manifest) with a `build` script. If yes, detect the package manager from the lockfile (`package-lock.json` β npm, `yarn.lock` β yarn, `pnpm-lock.yaml` β pnpm, `bun.lock`/`bun.lockb` β bun; default to npm if no lockfile) and run:
> ```powershell
> cd {component-path} # e.g., web/
> {pm} install # npm install, yarn install, pnpm install, etc.
> {pm} run build # npm run build, yarn build, pnpm build, etc.
> ```
> The deploy path should point to the **build output directory** β read the framework config to determine the output dir (`vite.config.*` β `build.outDir`, `next.config.*` β `.next/` or `out/`, CRA β `build/`; default to `dist/`). If no `package.json` exists (plain HTML/CSS/JS), deploy the source directory directly β no build needed.
>
> β **This is NOT optional for SPA frameworks.** Raw JSX/TSX/Vue/Svelte source files cannot be served by SWA β the app must be compiled to static HTML/JS/CSS first. Skipping the build produces a broken deployment.
> β **Pre-deploy: Update frontend config with deployed backend URLs.** If `prereq-output.json.cloudSdkSwaps[]` mapped cloud SDK endpoints (AWS API Gateway, GCP Cloud Functions) to Azure equivalents, the frontend config file (`config.ts`, `.env`, `environment.ts`) still references the original cloud URLs. After IaC deploy (Step 6), read `deploy-result.json.endpoints[]` to get the deployed Azure backend URLs. Update the frontend config with these URLs before running `swa deploy`. This is a string replacement in the config file β NOT a code rewrite. Do NOT defer as a post-deploy step β the SWA will show a broken page if the frontend calls non-existent AWS/GCP endpoints.
| Step | Command | Notes |
|------|---------|-------|
| 1. Get token | `$token = az staticwebapp secrets list --name {swa} -g {rg} --query "properties.apiKey" -o tsv 2>$null` | Store in variable first β do NOT pass inline. β **On Windows, use `2>$null`** (not `2>&1`) β Azure CLI Python warnings corrupt `-o tsv` output |
| 2. Set env var | `$env:SWA_CLI_DEPLOYMENT_TOKEN = $token` | β Use env var ONLY β do NOT pass `--deployment-token $token` as CLI arg (leaks token in process args / transcript) |
| 3. Copy to temp | `$tempDir = "C:\temp\swa-deploy"; Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue; New-Item -ItemType Directory -Path $tempDir -Force \| Out-Null; Copy-Item -Path .\* -Destination $tempDir -Recurse -Exclude @('.git','.copilot-azure','node_modules','.azure','infra')` | β **MANDATORY on Windows** β `$env:TEMP` often contains spaces (e.g., `C:\Users\Jane Doe\AppData\Local\Temp`). Always use a short, space-free path on the FIRST attempt β do NOT use `$env:TEMP` and retry. On macOS/Linux, use `/tmp/swa-deploy` instead. **If a build step ran:** copy the build output directory (e.g., `{component}/dist/`) instead of the entire workspace β `Copy-Item -Path {component}\dist\* -Destination $tempDir -Recurse`. |
| 4. Deploy | `swa deploy $tempDir --app-name {swaName} --env production` | β **`--app-name` is MANDATORY** β without it, the SWA CLI launches an interactive "create new project?" prompt that fails in automation. `{swaName}` = the SWA resource name from `prepare-plan.json.naming.resources[]`. SWA CLI reads `SWA_CLI_DEPLOYMENT_TOKEN` from env var automatically β do NOT pass `--deployment-token` flag (leaks token in command args) |
| 5. Clean up | `Remove-Item -Recurse -Force $tempDir` | Clean temp dir after successful deploy |
β **`az staticwebapp deploy` does NOT exist** β the correct CLI is `swa deploy` (from `@azure/static-web-apps-cli`).
## Fallback: Direct StaticSitesClient Upload
If `swa deploy` fails (binary crash, path errors on Windows), use the underlying `StaticSitesClient.exe` directly:
```powershell
# Find the binary bundled with @azure/static-web-apps-cli
$swaCliPath = (Get-Command swa).Source | Split-Path -Parent
$client = Get-ChildItem -Path $swaCliPath -Recurse -Filter "StaticSitesClient*" | Select-Object -First 1
# Upload from PARENT directory with relative paths (avoids "identical to artifact folder" error)
Push-Location (Split-Path $tempDir -Parent)
& $client.FullName upload --app (Split-Path $tempDir -Leaf) --apiToken $token --skipAppBuild true
Pop-Location
```
> β **Run from a PARENT directory** with a relative `--app` path. Running from inside the app directory causes `StaticSitesClient` to error with "Current directory cannot be identical to or contained within artifact folders."
## Finalize deploy-result.json (after `swa deploy` succeeds)
β **`swa deploy` succeeding is NOT the end of the deploy phase.** SWA finalizes through the **generic Step 8** (see [`deploy-checklist-template.md`](deploy-checklist-template.md) Β§"Before handoff (Step 8)") β overwrite the `deploy-result.json` skeleton IN PLACE with the full `DeployResult` contract, not a `status`+`subscriptionId` stub. Only the values Step 8 can't derive on the SWA path are below:
- **Hostname** β `swa deploy` produces no ARM endpoint output, so fetch it: `$swaHost = az staticwebapp show -n {swa} -g {rg} --query defaultHostname -o tsv`
- `endpoints[]` β `[{ name, url: "https://$swaHost", healthStatus: "healthy" }]` (HTTP GET `https://$swaHost/` β 2xx confirms healthy)
- `resourceIds[]` β include the `Microsoft.Web/staticSites` resource id
database-post-deploy.md 4.4 KB
# Database Post-Deploy Verification
Run schema migrations on AppOnboard-created databases (listed in `prepare-plan.json.services[]`) before health checks. The app must be running first β if it's crashing, fix that before attempting migrations.
## Create App Database (if needed)
> β **Azure PostgreSQL/MySQL Flexible Server only creates the system `postgres`/`mysql` database by default.** If the app's config references a named database (e.g., `car_sale_db`, `myapp_production`), create it BEFORE the container starts:
>
> ```powershell
> az postgres flexible-server db create -g {rg} -s {serverName} -d {dbName}
> ```
>
> Detect the database name from: (1) `prereq-output.json.initCommands[]` with `type: "db-migrate"`, (2) app config files (`config-docker.yml`, `.env`, `database.yml`), (3) compose `POSTGRES_DB` env var. If the container crashes with `database "X" does not exist`, this step was missed.
**Discover the migration command** from the codebase (check in order):
| Signal | Command | Working directory |
|--------|---------|-------------------|
| `alembic.ini` exists | `alembic upgrade head` | Directory containing `alembic.ini` |
| Django `manage.py` exists | `python manage.py migrate` | Directory containing `manage.py` |
| `prisma/schema.prisma` exists | `npx prisma migrate deploy` | Project root |
| EF `Migrations/` directory | `dotnet ef database update` | Project root |
| Rails `db/migrate/` directory | `rails db:migrate` | Project root |
| Sequelize `migrations/` + `.sequelizerc` | `npx sequelize-cli db:migrate` | Project root |
## Execute via the Deployed Environment
### App Service (Linux only)
```powershell
az webapp ssh -n {app} -g {rg} --subscription {sub}
# Then run the migration command interactively
```
### Container Apps
```powershell
az containerapp exec -n {ca} -g {rg} --subscription {sub} --command "{migration_command}"
# If the app's WORKDIR differs from migration tool location:
az containerapp exec -n {ca} -g {rg} --subscription {sub} --command "cd /app/backend && alembic upgrade head"
```
## Error Handling
If the migration command fails, classify as `IAC_ERROR` and check based on the database type:
- DB unreachable β check firewall rules (PostgreSQL: `AllowAllAzureServicesAndResourcesWithinAzureIps`, SQL: server firewall, MySQL: similar)
- Extension/feature missing β check DB-specific config (PostgreSQL: `azure.extensions`, SQL: compatibility level, MySQL: `require_secure_transport`)
- Module not found β verify the runtime includes the migration tool
> β **`{pass}` MUST be the same password passed to `az deployment sub create --parameters pgAdminPassword={value}`.** See deploy-safety.md Β§ Deploy Checklist β generate each secret ONCE, persist to `deploy-secrets.env`, reuse everywhere. Mismatched passwords cause silent auth failures on migrations and connectivity checks.
## PostgreSQL-Specific Checks
Run BEFORE migrations when `services[]` includes PostgreSQL Flexible Server:
1. **Firewall connectivity:** `az postgres flexible-server execute -n {pg} -g {rg} -u {admin} -p {pass} -d postgres --querytext "SELECT 1"` β if this fails, the firewall rule is missing or RBAC propagation hasn't completed. Check `AllowAllAzureServicesAndResourcesWithinAzureIps` exists, wait 60s, retry
2. **Extension availability:** `az postgres flexible-server parameter show -g {rg} -n {pg} --name azure.extensions --query value -o tsv` β verify the extensions the app needs (e.g., `uuid-ossp` for Alembic/Django UUID fields) are in the allow-list. If missing, the Bicep module should have set them β check `infra/modules/postgresql.bicep`
## MySQL Flexible Server Checks
Run BEFORE migrations when `services[]` includes MySQL Flexible Server:
1. **Firewall connectivity:** `az mysql flexible-server execute -n {mysql} -u {admin} -p {pass} -d mysql -q "SELECT 1"` β if this fails, check the firewall rule and RBAC propagation. Note: `execute` resolves by server name (no `-g` needed)
2. **SSL enforcement:** `az mysql flexible-server parameter show -g {rg} -n {mysql} --name require_secure_transport --query value -o tsv` β verify matches the app's connection string SSL mode
## Azure SQL Checks
Run BEFORE migrations when `services[]` includes Azure SQL:
1. **Firewall connectivity:** `az sql db show -g {rg} -s {sqlServer} -n {dbName} --query status -o tsv` β verify the database is `Online`
2. **Server firewall:** `az sql server firewall-rule list -g {rg} -s {sqlServer} -o table` β verify `AllowAllWindowsAzureIps` (0.0.0.0 β 0.0.0.0) exists for Azure-internal access
deploy-checklist-template.md 10.0 KB
# Deploy Checklist Template (compaction-safe β generated at Step 5b)
Long-running deploy sessions lose rules when the conversation compacts. At Step 5b, generate a checklist file tailored to this deployment. Write it to disk so it survives compaction β re-reading costs ~100 tokens.
**Write** to `.copilot-azure/sessions/{id}/deploy-checklist.md` using the `create` tool at Step 5b.
**Re-read** via `view` after every long-running command (`az deployment`, `az webapp deploy`, `az acr build`), after each failed health check, and after any conversation compaction.
## How to generate
Read `prepare-plan.json` to determine the service types, then build the checklist from the template below. **Replace `{placeholders}` with real values** and **delete sections that don't apply** (e.g., remove the App Service section for a Container Apps deploy).
```markdown
# Deploy Checklist for {appName}
# RG: {rgName} | Sub: {subscriptionId} | Session: {sessionId}
## β Secret generation (BEFORE first az deployment)
- Auto-generate ALL `@secure()` params before first `az deployment sub create` β NEVER `ask_user`
- β On ANY retry OR redeploy (incl. after a conversation compaction): read the SAME `@secure()` value back from Key Vault (source of truth) or `deploy-audit.log` β NEVER regenerate. A secret that's both applied to a resource AND stored in KV desyncs if regenerated: e.g. a DB module re-applying `administratorLoginPassword` re-sets the server admin but not the KV secret the app reads β auth 500s while provisioning still reports success.
## β Read deploy/SKILL.md
- You MUST `view` deploy/SKILL.md BEFORE running any `az deployment` command
- Path: `plugin/skills/azure-app-onboard/deploy/SKILL.md`
- If you have not read it in this conversation (or since the last compaction), read it NOW
- It covers preflight checks, portal links, what-if, SCM lifecycle, deploy-result.json schema, audit logging, and health checks β skip it and none of these happen
## After every `az` command
- Append 2 lines to `deploy-audit.log`: `{timestamp} | {command} | started` then `{timestamp} | {command} | succeeded/failed`
## After IaC deployment (Step 6)
- Verify 5 tags: `az group show -n {rgName} --query tags`
- β Do NOT set startup command or app settings via CLI β they are already in Bicep from scaffold. If `az webapp show` doesn't reflect them yet, wait 30s and re-check (ARM propagation delay). Do NOT run `az webapp config` imperatively.
Required: app-onboard-skill, app-onboard-session-id, created-at, environment, deployed-by
- Verify portal link is still correct if healing changed the deployment name
## Code deploy β App Service (delete if not using App Service)
- β **Deploy command: `az webapp deploy --type zip`** (Entra-capable, supports `--async`). NEVER `az webapp deployment source config-zip` β it needs SCM basic auth and is disallowed.
- Wait for stabilization: `az webapp show -g {rgName} -n {appName} --query state` β "Running"
- Verify `SCM_DO_BUILD_DURING_DEPLOYMENT=true` is active before deploy (ARM timing can delay)
- If build reports "0 seconds" but app needs deps: re-set the setting, wait 10s, retry
- If 0s persists after 2 retries: fall back to Kudu `/api/zipdeploy`
- Python: if no `antenv/` after deploy, use Kudu `/api/zipdeploy` immediately (OneDeploy may skip Oryx)
- Windows zip paths: normalize with `.Replace('\', '/')` before creating zip entries
- β Verify ORYX_DISABLE_COMPRESSION=true is set (prevents output.tar.zst extraction failures at startup β applies to ALL tiers, not just F1)
- Set WEBSITES_CONTAINER_START_TIME_LIMIT=1800 for safety
- TypeScript apps: verify `typescript` and `@types/*` are in `dependencies` (not devDependencies) β Oryx with NODE_ENV=production skips devDeps. Alternative: set app setting NPM_CONFIG_PRODUCTION=false
- Enable SCM before zip deploy, re-disable after: `az rest --method put` β allow:false β verify
- After deploy: check response body for Azure default page ("Your app service is up and running" = app didn't start)
## Code deploy β Container Apps (delete if not using Container Apps)
- Phase 2 is NOT optional β deploy actual image, don't leave placeholder
- Wait ~60s for RBAC propagation (AcrPull role) before code deploy
- BuildKit Dockerfiles: create Dockerfile.azure without --mount syntax
- Pass real image on EVERY Bicep redeploy: --parameters containerImage='{acr}/{app}:latest'
- KV secrets: `revision restart` does NOT refresh β must create new revision
- ACR build failures count toward healing counter
- Windows: append `--no-logs` to `az acr build` to avoid UnicodeEncodeError
- β After the revision is ready, run an explicit live HTTP probe against the ingress FQDN (`curl -sSfL`/`iwr` on `*.azurecontainerapps.io`) β this call IS the health check; capture the result into `deploy-result.json.endpoints[].healthStatus`.
## Code deploy β Static Web Apps (delete if not using SWA)
- β **Build before deploy (SPA only):** If the SWA component has a manifest (`package.json`) with a `build` script: detect the package manager from the lockfile, run install + build, then deploy the build output directory (not raw source). Plain HTML repos (no manifest): skip build, deploy source directly.
- β **Build-time env vars:** Before `npm run build`, set `VITE_API_BASE_URL` / `NEXT_PUBLIC_API_URL` / `REACT_APP_API_URL` to the deployed backend URL from `deploy-result.json.endpoints[]`. These are baked into the JS bundle at build time β runtime SWA env vars have no effect on client-side code.
- If frontend config references cloud SDK endpoints (AWS API Gateway, GCP), update with deployed Azure backend URLs from `deploy-result.json.endpoints[]` before `swa deploy`
- Use `swa deploy` (NOT `az staticwebapp deploy` β doesn't exist)
- `--app-name {swaName}` is mandatory
- Store token in $env:SWA_CLI_DEPLOYMENT_TOKEN β never as CLI arg
## During healing / retries
- β REGION LOCK: Deploy region MUST match plan region ({region}). Any region change β RE-PRESENT deploy approval gate with old and new region. Do NOT silently switch. After approval: update `prepare-plan.json.services[].region`, `deploymentVariables.location`, AND append attempt number to `naming.suffix` (e.g., `edd6` β `edd602`). Recompute ALL resource names from the new suffix before redeploying β globally unique names (App Service, Key Vault) from the old region may be soft-deleted and unavailable.
- β IaC-only: NEVER use `az containerapp update --image`, `az webapp update`, `az appservice plan delete`, or `az group create` β fix the Bicep and redeploy via `az deployment sub create`
- β IaC-only for app-managed roles: NEVER `az role assignment create` for AcrPull or KV Secrets User β Bicep-managed (deterministic GUID), so an imperative grant collides on redeploy (`RoleAssignmentExists`). Missing app role = fix the Bicep module and redeploy. (Deployer/subscription-scope 403s are the ONLY exception β see [`error-classification.md`](error-classification.md).)
- β **On error: read [`error-classification.md`](error-classification.md)** to classify the failure and follow the prescribed remediation. Do NOT ad-hoc heal without reading the classification.
- β **Never weaken a security control to unblock** β do NOT flip `require_secure_transport`/TLS, HTTPS-only, KV purge protection, or auth OFF to make a failing deploy pass. A DB TLS handshake failure = fix the client SSL config (prereq `W-MYSQL-SSL`) or ask the user; never downgrade the server.
- Count ALL attempts in deploy-result.json.healingAttempts[]
After 3: STOP and ask user ("Yes / I have a suggestion / Stop")
- NEVER run `az group delete` β track in orphanedResourceGroups[]
- β **RG deletion timeout:** If you ran `az group delete --no-wait`, wait max 2 minutes then `ask_user`: "Resource group deletion is slow. Wait longer / Proceed without cleanup / Cancel." Do NOT poll indefinitely.
- Region/SKU/service changes require re-approval gate
## Before handoff (Step 8)
- β Read [`deploy-schemas.ts`](deploy-schemas.ts) for exact DeployResult field names
- Finalize `deploy-result.json` β overwrite skeleton IN PLACE (keep exact field names, do NOT rename): status (lowercase `succeeded`/`failed`), resourceGroupName, subscriptionId, deploymentNames (all used), resourceIds, endpoints, healthStatus (worst across endpoints), duration.completedUtc, resourceResults from `az deployment operation list`. Read back to verify.
- β `deployment-summary.md` β generate from `deploy-result.json` fields (Status, Health, Portal Links, Cleanup). NOT a separate data source.
- β `context.json` β add "deploy" to completedPhases, set currentPhase to null, update lastModifiedUtc. VERIFY by reading back.
- SCM re-disabled (App Service) or image param set (Container Apps)
- If prereq found migration frameworks: run migrations before declaring healthy
## Artifact verification (Step 8 β MANDATORY)
β Before returning to orchestrator, verify ALL artifacts exist by reading each one back:
1. `deploy-result.json` β MUST contain (exact names): `status` (lowercase `succeeded`/`failed`), `resourceGroupName`, `subscriptionId`, `deploymentNames[]`, `resourceIds[]`, `endpoints[]`, `healthStatus`, `duration.completedUtc`, `resourceResults[]`. Missing/renamed fields β rewrite with real values NOW
2. `deploy-audit.log` β MUST exist with β₯2 entries (started + result for at least 1 command). Missing β reconstruct from memory
3. `deployment-summary.md` β MUST contain Status, Health, Portal Links sections. Missing β generate from deploy-result.json
4. `context.json` β MUST have `"deploy"` in `completedPhases`, `currentPhase: null`, updated `lastModifiedUtc`
5. β **Endpoint completeness** β EVERY service in `prepare-plan.json.services[]` that hosts application code MUST have a corresponding entry in `deploy-result.json.endpoints[]` with code deployed and a valid `healthStatus` (`healthy`, `degraded`, `unreachable`, `unknown`). If ANY compute endpoint is missing or has code not deployed, set `partial: true` and `status: "failed"`. A deployment with undeployed user components is NOT `"succeeded"`.
If ANY artifact is missing or incomplete, write it NOW β do NOT return to orchestrator without all 5 checks passing.
β **Then STOP β return to orchestrator. No further CLI commands or skill invocations.**
```
deploy-safety.md 4.5 KB
# Deploy Safety
Hook-based safety rules for the deploy phase. Block destructive operations.
## deploy-result.json Skeleton
Created by scaffold validate sub-agent. If missing at deploy Step 5b, create from [`deploy-schemas.ts`](deploy-schemas.ts) with `status: "in-progress"`. Append to `deploymentNames[]` on each healing retry.
## Deploy Checklist (compaction-safe β generated at Step 5b)
> β **You MUST read [`deploy-checklist-template.md`](deploy-checklist-template.md)** at Step 5b to generate the checklist. Write the result to `.copilot-azure/sessions/{id}/deploy-checklist.md`. Re-read the generated checklist after every long-running command, failed health check, and conversation compaction.
## Finalize deploy-result.json (Step 8)
Overwrite the skeleton with real values:
- `status` β `"succeeded"` or `"failed"`
- `deploymentNames` β ALL names used (initial + retries)
- `healthStatus` β worst across endpoints
- `duration.completedUtc` β now
- `resourceResults` β one entry per resource from `az deployment operation list`
## Blocked Patterns
> β **You MUST read [`blocked-patterns.md`](blocked-patterns.md)** before running ANY `az` command during the deploy phase. This file contains every command the agent is forbidden from executing. Block decisions are non-negotiable β user must run blocked commands manually outside AppOnboard.
## 403 Scope Fallback
When `az deployment sub create` returns 403 (insufficient subscription-scope permissions), do NOT halt immediately:
1. **Restructure Bicep to RG-scope** β change `targetScope = 'subscription'` to resource-group scope, remove the `Microsoft.Resources/resourceGroups` resource.
2. **Create the RG via CLI** β `az group create -n {rg} -l {region} --tags app-onboard-skill=true app-onboard-session-id={sessionId} created-at={createdAt} environment={environmentName} deployed-by={deployedBy}`. All 5 AppOnboard tags MUST be included.
3. **Retry with `az deployment group create`** β use `--resource-group {rg}` instead of subscription scope.
4. **Regenerate portal link** for RG-scope β `$resId` must include `/resourceGroups/{rg}`: `$resId = "/subscriptions/{subscriptionId}/resourceGroups/{rg}/providers/Microsoft.Resources/deployments/$deploymentName"`. Re-run `Write-Output "LINK=$l"; Start-Process $l 2>$null` and print new bare URL.
5. **If retry ALSO fails with 403** β classify as `ENVIRONMENT_BLOCKING`. Surface required role: `az role assignment create --role Contributor --assignee {user} --scope /subscriptions/{sub}/resourceGroups/{rg}`.
## Deploy Checklist
> β **Use sync shells** so state persists. **Persist secrets** to `.copilot-azure/sessions/{id}/deploy-secrets.env` β generate each secret ONCE (URL-safe, no `/+=`), reload in every later shell. Never regenerate an existing key. Key Vault is the durable source of truth for every secret (the file is only a cross-shell reload cache and is git-ignored via `.copilot-azure/`). NEVER echo or log rendered secret values.
>
> β **URL-safe passwords required** when app uses URL-based connection strings. Forbidden chars: `# @ / ? % : & = + ;`.
> β **`az webapp deploy` does NOT support `--track-status`.**
> β **`az rest` on Windows PowerShell:** ALWAYS include `--headers "Content-Type=application/json"`.
> β **Suppress deployment output:** Add `--query properties.provisioningState -o tsv` to deployment commands. For `az acr build`, append `--no-logs`.
## Post-Deploy Tag Verification
After deployment, verify all 5 AppOnboard tags: `az group show -n {rg} --query tags -o json`. Re-apply missing via `az tag update`.
## Deployment Operation Polling
For deployments with >5 resources, poll every 30s: `az deployment operation list --name {name} --subscription {sub} --query "[?properties.provisioningState=='Failed']" -o table`. Wait for FULL completion before healing β collect ALL errors in one pass.
## Re-Approval Gates
Region, service type, or SKU changes from user-approved values β re-present approval gate. Resource name changes β informational only. Same-deployment retries β no re-approval.
## Antipatterns
β Do NOT `az group delete --no-wait` then `az group create` same name β background deletion takes 5-15 min and destroys the new RG. Use a different name or wait: `az group wait --name {rg} --deleted --timeout 900`.
## Artifact Reconciliation After Healing
After ANY healing that changes deployed resources, update `prepare-plan.json`, `scaffold-manifest.json`, and `context.json` to reflect actual state. Track orphaned RGs in `deploy-result.json.orphanedResourceGroups[]` immediately when switching to a new RG.
deploy-schemas.ts 3.3 KB
/**
* Deploy artifact schema β deploy-result.json.
* Read by deploy SKILL.md Step 8 (finalize artifacts).
*/
export type HealthStatus = "healthy" | "degraded" | "unreachable" | "unknown";
// βββ Deploy healing types ββββββββββββββββββββββββββββββββββββββββββββββββββββ
export type DeployErrorClassification = "IAC_ERROR" | "INFRA_TRANSIENT" | "ENVIRONMENT_BLOCKING";
export type DeployHealingPhase = "validation" | "deployment";
/** Tracks a resource group created during a healing attempt that is no longer
* the final deployment target. Surfaced at handoff for manual cleanup. */
export interface OrphanResourceGroup {
/** Azure resource group name */
name: string;
/** Region where the RG was created */
region: string;
/** Which healing attempt created or targeted this RG */
healingAttempt: number;
/** Why this RG was abandoned (e.g., "region fallback to westus2") */
reason: string;
}
export type DeployHealingAction = "routed-to-scaffold" | "retried" | "surfaced-to-user";
export type DeployHealingResult = "fixed" | "still-failing" | "blocked";
export interface DeployHealingError {
source: string;
detail: string;
classification: DeployErrorClassification;
}
export interface DeployHealingAttempt {
attempt: number;
phase: DeployHealingPhase;
errors: DeployHealingError[];
action: DeployHealingAction;
result: DeployHealingResult;
/** Resource group targeted by this attempt β useful for audit and debugging */
resourceGroupName?: string;
/** True when this healing attempt changed the service type or region β requires re-approval */
planLevelChange?: boolean;
/** What changed: "service-type", "region", "sku" */
changeType?: "service-type" | "region" | "sku";
/** Original value before the change (e.g., "App Service B1 eastus") */
originalValue?: string;
/** New value after the change (e.g., "Container Apps Consumption eastus") */
newValue?: string;
}
// βββ deploy-result.json ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export interface DeployEndpoint {
name: string;
url: string;
healthStatus: HealthStatus;
}
export interface DeployDuration {
startedUtc: string;
completedUtc: string;
}
export type DeployStatus = "in-progress" | "succeeded" | "failed";
export type ResourceDeployStatus = "succeeded" | "failed" | "skipped";
export interface ResourceResult {
resourceId: string;
type: string;
status: ResourceDeployStatus;
error?: string;
}
export interface DeployResult {
sessionId: string;
/** Azure subscription ID from context.json.azure.subscriptionId */
subscriptionId: string;
/** All ARM deployment names used during this session (initial + healing retries).
* First entry is the initial deployment; subsequent entries are from scope/RG changes. */
deploymentNames: string[];
resourceGroupName: string;
status: DeployStatus;
resourceIds: string[];
endpoints: DeployEndpoint[];
healthStatus: HealthStatus;
duration: DeployDuration;
warnings: string[];
partial: boolean;
resourceResults: readonly ResourceResult[];
/** RGs created during healing that are not the final deployment target.
* Surfaced at handoff (Step 9) with manual cleanup commands. */
orphanedResourceGroups: readonly OrphanResourceGroup[];
healingAttempts?: readonly DeployHealingAttempt[];
}
error-classification.md 3.6 KB
# Error Classification
Three error categories for deploy-time failures.
## Multi-Error Triage
> β When 3+ errors, classify ALL before fixing ANY. Group by root cause. Present: "Deploy failed with {N} errors from {M} root causes."
## Categories
### `IAC_ERROR` β Route Back to Scaffold
| Example | Fix |
|---------|-----|
| Invalid property name | Call `mcp_bicep_build_bicep` with `{ filePath: "infra/main.bicep" }` for structured error details, then fix. Fallback: `az bicep build` |
| Wrong SKU for region | Substitute from `rejectedAlternatives[]` |
| Missing required field | Call `mcp_bicep_build_bicep` for structured error, fix from diagnostics. Fallback: `az bicep build` |
| Policy violation | Substitute per policy |
| API version not found | Call `mcp_bicep_list_az_resource_types_for_provider` with `{ providerNamespace: "..." }`. Use latest GA β no `-preview`. Fallback: `az provider show` |
| `listKeys()` in output | β Security risk β replace with KV secret + MI reference |
| KV soft-delete collision | Rename KV (append suffix). If not viable β user purges manually |
| Redis `InvalidRequestBody` for `properties.sku.name` | Bicep type issue β create via `az redis create --sku Basic --vm-size c0`, switch Bicep to `existing` keyword |
**Flow:** Deploy β classifies IAC_ERROR β scaffold self-healing β re-validate β retry.
### `INFRA_TRANSIENT` β Retry with Backoff
| Example | Strategy |
|---------|----------|
| ARM 429, 409 conflict, RBAC delay, network timeout | 30s β 60s β 120s (3 max) |
| Container Apps `Operation expired` | Check root cause first: image pull failure, port mismatch, health probe timeout, crash loop. Port mismatch β IAC_ERROR. Image pull β retry. Crash β ENVIRONMENT_BLOCKING. |
After 3 failures β escalate to user.
### `ENVIRONMENT_BLOCKING` β Surface to User
| Example | Action |
|---------|--------|
| 403 on sub-scope deploy | β Try [403 Scope Fallback](deploy-safety.md) first (RG-scope). Only block if retry also fails |
| 403 on RG-scope | Surface: `az role assignment create --role Contributor --assignee {objectId} --scope {rg}` |
| AuthorizationFailed / deny assignment | Get user objectId, suggest role assignment command. Deny assignments β contact admin |
| Quota exhausted | β PLAN_LEVEL_CHANGE β HALT, present region fallback with re-approval |
| Region doesn't support resource | β PLAN_LEVEL_CHANGE β same as quota |
| `LocationIsOfferRestricted` | β PLAN_LEVEL_CHANGE β read `quotaValidation.offerRestrictions[]` for unblocked regions |
| MI sidecar OOM on F1/B1 | Upgrade SKU, remove MI, or switch to Container Apps |
| AADSTS530084 / TF auth failure | Re-scaffold as Bicep + `az deployment group create`. Never fall back to imperative CLI |
> β **During ALL healing: NEVER run `az group delete`, `az postgres flexible-server delete`, `az redis delete`, `az webapp delete`, or any destructive resource deletion.** Track failed RGs in `deploy-result.json.orphanedResourceGroups[]` instead. The user deletes at handoff. If you need a clean region retry, use a NEW RG name (append `-2`) β do NOT delete-and-recreate.
## Healing Trace
Log to `deploy-result.json.healingAttempts[]`: `{ attempt, phase, errors: [{ source, detail, classification }], action, result, planLevelChange, changeType, originalValue, newValue }`.
> β **Repeat failure (same error 2+):** Read [iac-resources.md](../../references/iac-resources.md) Β§ Deploy Troubleshooting and `fetch_webpage` the matching URL.
After 3 failed cycles: write `partial: true`, surface remaining errors. Do NOT auto-rollback.
## Known Platform Bugs / Deploy Timing
See [`pipeline-rules-runtime.md`](../../references/pipeline-rules-runtime.md).
health-check-patterns.md 5.4 KB
# Health Check Patterns
Post-deployment health verification for AppOnboard-deployed resources.
## HTTP Endpoints
For each endpoint: HTTPS GET, 30s timeout, 3 retries (10s/20s/40s backoff).
| Status | Health | Note |
|--------|--------|------|
| 2xx | `healthy` | **Verify not a placeholder page (see below)** |
| 401/403 | `healthy` | Auth working, app running |
| 5xx Γ3 | `degraded` | |
| Timeout/DNS Γ3 | `unreachable` | |
> β **DB-backed apps: a 200 on `/` is NOT healthy.** When `prepare-plan.json.services[]` includes a database, probing only `/` (or any non-DB route) just proves the web server booted. Probe at least one **data-backed route** (derive from the app's detected routes, e.g. a REST resource path) and inspect the body for DB errors (`insecure transport`, `Access denied`, `connection refused`, `Unknown database`, `doesn't exist`) β mark `degraded`, not `healthy`.
### HTTP Redirect Handling (Container Apps)
> β **ACA health probes do NOT follow HTTP redirects.** A 301/302 response from the probe path causes `ActivationFailed` β the probe treats it as a failure, not a redirect.
If the first health check returns **301 or 302**:
1. Read the `Location` header: `curl -sI "https://{fqdn}{probePath}" | Select-String "^location:" -CaseSensitive:$false`
2. Update `probePath` in Bicep to the redirect target (e.g., `/wetty/` β `/wetty`)
3. Redeploy: `az deployment sub create` with updated Bicep
4. Re-check health after new revision activates
Common redirect patterns: Express trailing-slash normalization (`/app/` β `/app`), framework-level path canonicalization, HTTPS redirects on mixed-content paths.
### App Service Default Page Detection
> β **HTTP 200 β app started.** Azure serves its own default page with 200 when the app fails to start β false positive.
After HTTP 200 from App Service, check first 2KB of body:
| Body contains | Meaning |
|---------------|--------|
| `"Your app service is up and running"` | Default page β app didn't start |
| `"Time to take the next step and deploy your code"` | Default page β no code or app failed |
| `"Hey, Python developers!"` / `"Hey, Node.js developers!"` | Runtime default β app didn't start |
| `"Error 503"` / `"Application Error"` | App crashed on startup |
If detected β `healthStatus: "degraded"` + warning: `"App Service default page detected β check logs: az webapp log tail -g {rg} -n {app}"`.
## Non-HTTP Resources
```bash
az resource show --ids {resourceId} --query "properties.provisioningState" -o tsv
```
`Succeeded` β `healthy`. `Failed` β `degraded`. Other β `unknown`.
| Service | Health Signal |
|---------|---------------|
| Container Apps | `latestReadyRevisionName` not empty + HTTP on ingress FQDN |
| App Service | HTTP GET `https://{name}.azurewebsites.net/` + `/health` |
| Static Web Apps | HTTP GET `https://{defaultHostname}/` β 2xx = `healthy` (hostname: `az staticwebapp show -n {swa} -g {rg} --query defaultHostname -o tsv`) |
| Azure SQL | `provisioningState` + `az sql db show` |
| Cosmos DB | `provisioningState` |
| Storage | `provisioningState` + `statusOfPrimary` |
| Key Vault | `provisioningState` |
| Functions | HTTP trigger URL + HTTP check |
> β **Container Apps β run an explicit live HTTP probe (the pipeline status is NOT sufficient).** After `latestReadyRevisionName` is set, run an observable request against the ingress FQDN and capture the result into `deploy-result.json.endpoints[].healthStatus`:
> ```powershell
> iwr "https://{ingressFqdn}/{probePath}" -UseBasicParsing # PowerShell
> curl -sSfL "https://{ingressFqdn}/{probePath}" # bash
> ```
> This live HTTP call against `*.azurecontainerapps.io` IS the health verification β do NOT infer health from the revision's internal status alone.
## Output
Write to `deploy-result.json.endpoints[]`:
```jsonc
{
"name": "api",
"url": "https://myapp-ca-dev-a1b2.azurecontainerapps.io",
"healthStatus": "healthy" // healthy | degraded | unreachable | unknown
}
```
Overall `healthStatus` = worst status across all endpoints. If any `unreachable` β overall `unreachable`.
## Functional Endpoint Verification
> β **A 200 on `/` only proves the web server booted β not that the app works.** After the HTTP check, confirm the app actually functions against the services the plan provisioned (database, cache, KV secrets), not just that it responds.
Health checks only confirm the web server is responding. Exercise a route that depends on the backing services β for example:
| Pattern | Functional Check |
|---------|-----------------|
| Database in the plan (MySQL/PostgreSQL/SQL/Cosmos) | Probe a route that reads/writes the DB (a detected app route, NOT `/` β root often serves a static page with no DB access, so 200 on `/` masks broken DB connectivity). A 5xx or a DB error in the body (`Access denied`, `connection refused`, `does not exist`) β `degraded` β usually a KVβDB credential mismatch or an unseeded secret. |
| `FIRST_SUPERUSER` env var or `prestart.sh`/`init_db()` | After health passes, attempt login endpoint. If 401/500 β startup scripts may have failed. Trigger `az containerapp revision restart` to re-run startup. |
| Migration frameworks (Alembic, Django, Prisma, EF) | After health passes, check `prereq-output.json` for migration signals. If found, run migrations per [database-post-deploy.md](database-post-deploy.md). |
| Two-phase Container Apps with KV secrets | Wait 60s after Phase 2 for RBAC propagation. If login/API fails with auth errors, KV secrets may not have resolved at revision startup. Create a new revision. |
mcp-tools.md 5.5 KB
# Deploy Phase β MCP Tools
Phase-exclusive tool parameters for the deploy phase. For shared tools (`subscription_list`, `group_list`, `extension_cli_install`, `get_azure_bestpractices`), see [mcp-tool-reference.md](../../references/mcp-tool-reference.md).
> **Troubleshooting:** If a tool call fails with unknown parameter or missing command errors, consult the official docs: <https://learn.microsoft.com/en-us/azure/developer/azure-mcp-server/tools/>
---
## `mcp_azure_mcp_deploy` (hierarchical β deploy-phase sub-commands)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `deploy_app_logs_get` | `workspace-folder`, `azd-env-name` | `limit` (default 200) | β
|
| `deploy_architecture_diagram_generate` | `workspaceFolder`, `services[]` (complex array with name, path, language, port, azureComputeHost, dependencies, settings) | `projectName` | β
|
## `mcp_azure_mcp_resourcehealth` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `resourcehealth_availability-status_get` | *(none)* | `subscription`, `resourceId` (full ARM ID), `resource-group` | β
|
| `resourcehealth_health-events_list` | *(none)* | `subscription`, `event-type` (ServiceIssue\|PlannedMaintenance\|HealthAdvisory\|Security), `status` (Active\|Resolved), `tracking-id`, `filter`, `query-start-time`, `query-end-time` | β
|
## `mcp_azure_mcp_monitor` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `monitor_activitylog_list` | `resource-name` | `resource-group`, `resource-type`, `hours`, `event-level` (Critical\|Error\|Informational\|Verbose\|Warning), `top`, `subscription` | β
|
| `monitor_metrics_query` | `resource`, `metric-names` (comma-sep), `metric-namespace` | `resource-group`, `resource-type`, `start-time`, `end-time`, `interval`, `aggregation` (Average\|Maximum\|Minimum\|Total\|Count), `filter`, `max-buckets` | β
|
| `monitor_metrics_definitions` | `resource` | `resource-group`, `resource-type`, `metric-namespace`, `search-string`, `limit` | β
|
| `monitor_workspace_list` | *(none)* | `subscription` | β
|
| `monitor_workspace_log_query` | `resource-group`, `workspace`, `table`, `query` (KQL or "recent"\|"errors") | `hours`, `limit`, `subscription` | β
|
| `monitor_resource_log_query` | `resource-id` (full ARM ID), `table`, `query` | `hours`, `limit`, `subscription` | β
|
## `mcp_azure_mcp_appservice` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `appservice_webapp_get` | *(none)* | `app`, `resource-group`, `subscription` | β
|
| `appservice_webapp_deployment_get` | `resource-group`, `app` | `deployment-id`, `subscription` | β
|
| `appservice_webapp_diagnostic_diagnose` | `resource-group`, `app`, `detector-name` (Availability\|CpuAnalysis\|MemoryAnalysis) | `start-time`, `end-time`, `interval`, `subscription` | β
|
| `appservice_webapp_diagnostic_list` | `resource-group`, `app` | `subscription` | β
|
| `appservice_webapp_settings_get-appsettings` | `resource-group`, `app` | `subscription` | β
(β οΈ secret) |
| `appservice_database_add` | `resource-group`, `app`, `database-type` (SqlServer\|MySQL\|PostgreSQL\|CosmosDB), `database-server`, `database` | `connection-string`, `subscription` | β |
| `appservice_webapp_settings_update-appsettings` | `resource-group`, `app`, `setting-name`, `setting-update-type` (add\|set\|delete) | `setting-value`, `subscription` | β (destructive) |
## `mcp_azure_mcp_role` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `role_assignment_list` | `scope` (ARM scope path) | `subscription` | β
|
---
## Phase 4 Tool Map
| Tool | Sub-command | AppOnboard Step | Purpose |
|------|-----------|----------|---------|
| `mcp_azure_mcp_resourcehealth` | `resourcehealth_availability-status_get` | Step 7 | Post-deploy health. Call with `resourceId` from `deploy-result.json.resourceIds[]` |
| `mcp_azure_mcp_resourcehealth` | `resourcehealth_health-events_list` | Step 3 | Pre-deploy outage check. `event-type: "ServiceIssue"`, `status: "Active"` |
| `mcp_azure_mcp_monitor` | `monitor_activitylog_list` | Step 7+ | Failed deployment analysis. `resource-name` from deploy output, `event-level: "Error"`, `hours: 1` |
| `mcp_azure_mcp_monitor` | `monitor_metrics_query` | Step 7 | Performance validation. Requires `metric-namespace` from `monitor_metrics_definitions` first |
| `mcp_azure_mcp_appservice` | `appservice_webapp_get` | Step 7 | Verify App Service state, hostnames, runtime |
| `mcp_azure_mcp_appservice` | `appservice_webapp_deployment_get` | Step 7 | Verify deployment completed successfully |
| `mcp_azure_mcp_appservice` | `appservice_webapp_diagnostic_diagnose` | Step 7 | Run `Availability` detector on deployed app |
| `mcp_azure_mcp_deploy` | `deploy_app_logs_get` | Step 7 | App logs for azd-deployed apps only. Requires `workspace-folder` + `azd-env-name` |
| `mcp_azure_mcp_role` | `role_assignment_list` | Step 3 | Preflight RBAC check. `scope: "/subscriptions/{sub}/resourceGroups/{rg}"` |
| `mcp_azure_mcp_subscription_list` | *(flat)* | Step 1 | Resolve target subscription |
| `mcp_azure_mcp_group_list` | *(flat)* | Step 3 | Verify RG exists before deploy |
| `mcp_azure_mcp_extension_cli_install` | *(flat)* | Step 3 | Ensure az/azd/func CLI installed |
portal-links.md 3.0 KB
# Portal Monitoring Links
Generate the portal link BEFORE deploying β the deployment name is deterministic, so the link works before the deployment even starts. The user wants to watch resources being created in real time.
## Generate via PowerShell β NEVER construct manually
The `%2F` encoding is critical and models consistently decode it back to `/` during text generation, producing broken links. Always use `.Replace('/', '%2F')`.
```powershell
# Subscription-scope deployment
$deploymentName = "app-onboard-deploy-$("{sessionId}".Substring(0,8))"
$resId = "/subscriptions/{subscriptionId}/providers/Microsoft.Resources/deployments/$deploymentName"
$link = "https://portal.azure.com/#view/HubsExtension/DeploymentDetailsBlade/~/overview/id/$($resId.Replace('/', '%2F'))"
Write-Output "LINK=$link"
Start-Process $link 2>$null
```
Read `LINK=` from terminal output and print the URL in chat on its own bare line β no backticks, no markdown. Then deploy:
```powershell
az deployment sub create --name $deploymentName --subscription {subscriptionId} --location {location} --template-file infra/main.bicep --parameters @infra/main.parameters.json
```
### RG-scope (403 fallback)
Use `az deployment group create --name $deploymentName --resource-group {rg}` and adjust `$resId` to include `/resourceGroups/{rg}`:
```powershell
$resId = "/subscriptions/{subscriptionId}/resourceGroups/{rg}/providers/Microsoft.Resources/deployments/$deploymentName"
$link = "https://portal.azure.com/#view/HubsExtension/DeploymentDetailsBlade/~/overview/id/$($resId.Replace('/', '%2F'))"
Write-Output "LINK=$link"
Start-Process $link 2>$null
```
For Terraform (no single ARM deployment): `https://portal.azure.com/#@{tenantId}/resource/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/activitylog`
> π‘ Resolve `{subscriptionId}` and `{resourceGroup}` from `context.json`. For Terraform, resolve `{tenantId}` via `az account show --query tenantId -o tsv`.
## Same-Scope Retries vs New Names
The portal link stays valid for same-scope retries β ARM overwrites in-place. Generate a new name (e.g., `$deploymentName = "app-onboard-deploy-{first8}-2"`) **only** when scope or RG changes.
## Chat Output Rules
1. **Terminal command:** the PowerShell snippet above (outputs the bare URL and auto-opens it in the default browser via `Start-Process`)
2. **Read the terminal output** β find the line starting with `LINK=` and extract the URL
3. **Chat output:** paste the bare URL on its own line β no backticks, no markdown, no emoji on the same line
β **Link must be ctrl+clickable.** The URL MUST be the ONLY content on its line β no emoji, no text, no backticks, no markdown formatting, no markdown link syntax on the same line. Terminals auto-linkify bare URLs but ONLY when the URL is alone on the line.
β **Emit a NEW link whenever deployment name changes.** When healing causes a redeploy with a different `--name`, you MUST re-run the PowerShell snippet with the new name and print the new link:
```
β οΈ Previous deployment link is stale β use this one:
https://portal.azure.com/.../{newDeploymentName}
```
preflight-checks.md 4.3 KB
# Preflight Checks
Pre-deployment validation steps. Run after user approval, before deployment execution.
> AppOnboard runs direct deployment (no `azd`).
## Check Sequence
Branch on `scaffold-manifest.json.iacFormat`:
### 0. Auth Token Verification
```bash
az account show
```
- Success β proceed. Active subscription + tenant confirmed.
- Failure β `ENVIRONMENT_BLOCKING`. Suggest `az login` (plain, no scope).
- β NEVER suggest `az login --scope https://graph.microsoft.com/.default` β Graph scope is irrelevant for ARM deployments.
### 0b. Resource Name Availability
Check globally-unique names before deploy: `az acr check-name`, `az storage account check-name`, `az webapp show`, `az keyvault show`. Name taken β suggest alternate from `prepare-plan.json.naming.suffix`: "Name `{name}` taken. Use `{altName}`?"
### 0c. F1/Free Tier Warning
If plan includes F1/D1/free SKUs, surface at deploy gate (do NOT block):
> β οΈ Free tier: no custom domains, no SSL, no always-on, 60 min/day compute (F1). Dev/test only.
### 0d. RBAC Scope Pre-Check
```bash
az role assignment list --assignee {userId} --scope /subscriptions/{sub} --query "[].roleDefinitionName" -o tsv
```
Subscription-scope deploy requires `Contributor`/`Owner` on subscription. Missing β `ENVIRONMENT_BLOCKING` with `az role assignment create` command.
### 1. Deployment Preview
β **MANDATORY β do NOT skip.** What-if validates + previews in one call. Use `what-if` exclusively β `az deployment sub/group validate` hits a known CLI bug (HTTP stream consumed error). If what-if fails, log + warn user β do not skip to execution.
#### Bicep (subscription scope)
```bash
az deployment sub what-if \
--name "{deploymentName}" \
--location {location} \
--template-file infra/main.bicep \
--parameters @infra/main.parameters.json \
--subscription {subscriptionId} \
--what-if-result-format FullResourcePayloads
```
#### Bicep (resource-group scope)
```bash
az deployment group create \
--resource-group {rg} \
--template-file infra/main.bicep \
--parameters @infra/main.parameters.json \
--subscription {subscriptionId} \
--what-if \
--what-if-result-format FullResourcePayloads
```
- Review changes: `Create`, `Modify`, `Delete`, `NoChange`. Surface `Delete` as warnings β user must acknowledge.
- Auth error β `ENVIRONMENT_BLOCKING`.
#### Terraform
```bash
terraform plan -out=tfplan -detailed-exitcode
```
- Exit 0 β no changes. Exit 2 β changes (normal). Exit 1 β error.
- Surface `destroy` as warnings β user must acknowledge. Auth error β `ENVIRONMENT_BLOCKING`.
### 3. RBAC Permission Check
```bash
az role assignment list \
--assignee {currentUserObjectId} \
--scope /subscriptions/{sub}/resourceGroups/{rg} \
--query "[].roleDefinitionName" -o tsv
```
Required: `Contributor` or `Owner` on the target resource group. If missing β `ENVIRONMENT_BLOCKING` with remediation command.
### 4. SKU Quota Verification
β **`what-if` does NOT catch quota errors.** It returns `Succeeded` even when target SKU has limit=0.
If `prepare-plan.json.quotaValidation.verified == true` β proceed.
Otherwise β **read [sku-quota-validation.md](../../prepare/references/sku-quota-validation.md)** and run direct quota checks NOW (per-provider API patterns, offer restrictions). If limit=0 β HALT, present region fallback. Skip regions in `quotaValidation.checkedRegions` with zero availability.
β `SubscriptionIsOverQuotaForSku` or `LocationIsOfferRestricted` in deploy output β HALT. See [error-classification.md](error-classification.md).
### 5. Resource Group Existence
```bash
az group show --name {rg} --query "location" -o tsv 2>/dev/null
```
- Exists β verify location matches `prepare-plan.json` region. Mismatch β warn.
- Not exists β will be created by deployment (if `main.bicep` has subscription scope).
## Error Handling
Each check runs independently. Collect all results, then present structured report.
| Check | Fail Behavior |
|-------|---------------|
| Deployment preview | Warn, don't block (can fail on unsupported types) |
| RBAC | Block. Surface `az role assignment create`. |
| RG check | Warn on location mismatch. Don't block. |
## Report Format
```
## Preflight Results
β
IaC syntax: valid (terraform validate / bicep build)
β οΈ Deployment preview: 3 creates, 0 destroys, 1 update
β
RBAC: Contributor role confirmed
β
Resource group: rg-myapp-dev (eastus2)
```
subagent-preflight.md 5.0 KB
# Subagent Template β Deploy Preflight & Checklist Generation
Read deploy reference files and distill deployment-specific rules into `deploy-checklist.md`. This is the main agent's ONLY source of deploy rules.
## Critical Rules
- β Do NOT invoke ANY skills, run `az` commands, or modify IaC/app code. Read-only sub-agent.
- β Do NOT read `deploy-schemas.ts` or `error-classification.md` β main agent reads those on-demand.
## Input (provided by caller)
| Field | Source |
|-------|--------|
| `prepare-plan.json` content | services[], naming, region, costEstimate, deploymentVariables |
| `scaffold-manifest.json` content | deployCommand, validationResult, files[] |
| `context.json` content | azure.subscriptionId, subscriptionName, resourceGroup, sessionId, intent |
| `prereq-output.json` content | buildRequirements, warnings[], components[] |
| Session folder path + working directory | Required |
## Output
| Artifact | Location |
|----------|----------|
| `deploy-checklist.md` | Session folder |
| `deploy-result.json` skeleton (if missing) | Session folder |
| Summary (β€300 tokens) | Return to caller |
## Workflow
### Step 1 β Read session artifacts + write skeleton
Read all 4 session artifacts. Extract: services[], naming, costEstimate, deployCommand, validationResult, deploymentVariables, subscriptionId, sessionId, buildRequirements, warnings[], quotaValidation.
If `deploy-result.json` missing, write skeleton with these EXACT field names (do NOT rename): `{ sessionId, subscriptionId, resourceGroupName, deploymentNames: ["app-onboard-deploy-{first 8 of sessionId}"], status: "in-progress", startedUtc, resourceIds: [], endpoints: [], healthStatus: "unknown", resourceResults: [], healingAttempts: [] }`.
### Step 2 β Read safety + blocked patterns refs
Read [deploy-safety.md](deploy-safety.md) and [blocked-patterns.md](blocked-patterns.md). Bake into checklist sections: deploy-result.json rules, blocked commands table, shell rules (sync shells, secrets persistence, no --track-status, az rest headers on Windows), 403 scope fallback (4-step procedure with real values), post-deploy tag verification, deployment operation polling (conditional: >5 resources), re-approval gates, antipatterns, artifact reconciliation.
### Step 3 β Read preflight + approval gate refs
Read [preflight-checks.md](preflight-checks.md) and [approval-gate-template.md](approval-gate-template.md).
Bake preflight into checklist: auth token check, resource name availability (real names), RBAC scope pre-check, β MANDATORY what-if command (pre-filled with real deploymentName, region, subscriptionId), RG existence check, offer restriction check (conditional: if `offerRestrictionsVerified` false AND DB services in plan).
Bake approval gate VERBATIM with real values: subscription, RG, region, service table, cost table, validation status, files list, response handlers with exact CLI commands. If F1/D1 detected, append warning.
### Step 4 β Read code-deployment + health refs
Read ONLY the code-deployment ref(s) matching the compute types in `prepare-plan.json.services[]`. **Do NOT read** references for compute types absent from the plan:
- If `App Service` or `Functions` in plan β read [code-deployment-appservice.md](code-deployment-appservice.md)
- If `Container Apps` in plan β read [code-deployment-container-apps.md](code-deployment-container-apps.md)
- If `Static Web Apps` in plan β read [code-deployment-swa.md](code-deployment-swa.md)
Bake per-service-type `## Code deploy` sections (one per compute service β do NOT merge).
Read [health-check-patterns.md](health-check-patterns.md). Bake health check section: HTTP checks (timeout 30s, 3 retries), status interpretation, Azure default page detection strings, non-HTTP resource checks, functional verification (conditional).
### Step 5 β Read conditional refs + handoff + write checklist
**Database (conditional):** If DB services in plan β read [database-post-deploy.md](database-post-deploy.md). Bake migration discovery, execution commands, PG-specific checks.
Read [../../references/handoff-protocol.md](../../references/handoff-protocol.md). Bake cleanup commands (with real rg, sessionId), orphan listing, healing summary, post-deploy recommendations, skill-based next steps, auth-aware handoff.
**Write `deploy-checklist.md`** to session folder. If it already exists, replace all content via `edit`; if not, use `create`:
```
# Deploy Checklist for {appName}
# RG: {rgName} | Sub: {subscriptionId} | Session: {sessionId}
# β οΈ If compaction recently occurred: re-read deploy/SKILL.md Steps 6-8
```
Delete inapplicable sections (e.g., remove App Service section for Container Apps deploys).
> β **Copy `## Before handoff (Step 8)` and `## Artifact verification (Step 8 β MANDATORY)` from the template VERBATIM.** Do NOT paraphrase, merge, or weaken `β` markers. These sections are the compaction-safe finalization anchor β diluting them causes artifact writes to be skipped after compaction.
### Step 6 β Return summary
Return β€300 tokens: preflight warnings, deploy command, service types, confirmation of checklist write, database migration note if applicable.
SKILL.md 6.1 KB
# Prepare β Architecture Planning & Cost Estimation
## Quick Reference
| Property | Value |
|----------|-------|
| Best for | Mapping app components to Azure services with cost estimation and quota validation |
| Inputs | `prereq-output.json` + `context.json` from `.copilot-azure/sessions/{id}/` |
| Outputs | `prepare-plan.json` written to session directory |
| Parent | [azure-app-onboard](../SKILL.md) |
## When to Use This Skill
Invoked by the `azure-app-onboard` orchestrator at Phase 2 when `prereq-output.json` exists. Not directly user-routable.
> **Return to orchestrator:** When complete, return control to `azure-app-onboard`. Do NOT directly invoke scaffold or deploy.
## When NOT to Use
| Scenario | Use Instead |
|----------|-------------|
| Code readiness or prereq scanning | `azure-app-onboard` Step 3 (prereq) |
| IaC generation from a completed plan | `azure-app-onboard` Step 7 (scaffold) |
| Deploying resources to Azure | `azure-app-onboard` Step 9 (deploy) |
| Optimizing existing Azure spend | `azure-cost` |
| Estimating VM-specific costs | `azure-compute` |
| Enterprise landing zone architecture | `azure-enterprise-infra-planner` |
## MCP Tools
| Tool | Purpose |
|------|----------|
| `mcp_azure_mcp_pricing` / `azure-pricing` (router β `command: pricing_get`) | Cost estimation (inline β see Step 6). Fallback: dispatch [`subagent-pricing.md`](references/subagent-pricing.md) |
| `mcp_azure_mcp_policy` | Subscription policy constraints |
| `az rest` | Quota validation (via sub-agent β see Step 5) |
| `mcp_azure_mcp_cloudarchitect` β `cloudarchitect_design` | WAF-aligned architecture design |
| `mcp_azure_mcp_wellarchitectedframework` | Per-service WAF guidance |
| `mcp_azure_mcp_advisor` β `advisor_recommendation_list` | Optimization recommendations |
## Workflow
| # | Step | Action | Reference |
|---|------|--------|-----------|
| 1 | **Read session state** | Load `prereq-output.json` + `context.json`. Resolve subscription | Cross-ref [subscription-resolution.md](../references/subscription-resolution.md) if needed |
| 2 | **Query policy constraints** | Inline MCP: fetch policy + advisor recommendations | `mcp_azure_mcp_policy` + `mcp_azure_mcp_advisor` |
| 3 | **Map components to services** | Per-component Azure service selection, Dockerfile routing, deploy-as-is | β **You MUST read [service-mapping.md](references/service-mapping.md) and [deploy-strategy.md](references/deploy-strategy.md)** |
| 4 | **Select SKUs + WAF analysis** | Budget-aware SKU selection, inline WAF service guidance | β **You MUST read [sku-matrix.md](references/sku-matrix.md)** |
| 5 | **Validate quotas + region capacity** | β Read [`subagent-quota.md`](references/subagent-quota.md) β dispatch as `task` (NEXT action MUST be `task`, β agent_type: `"task"` β NEVER `"general-purpose"`). Copy the **COMPLETE and UNMODIFIED** template text into the task prompt between `<<<TEMPLATE_START>>>` / `<<<TEMPLATE_END>>>` delimiters β do NOT summarize. Append the caller-provided inputs listed in [`subagent-quota.md`](references/subagent-quota.md)'s Input table AFTER the template block. β **After dispatching, proceed to Step 6 (cost estimation) while the subagent runs. Do NOT run quota checks yourself β the subagent handles it. Collect subagent results before Step 9 (write plan).** | β **You MUST read [`subagent-quota.md`](references/subagent-quota.md)** |
| 6 | **Estimate costs** | β **You MUST read [pricing-guide.md](references/pricing-guide.md)** for methodology, then [pricing-guide-services.md](references/pricing-guide-services.md) for per-service filters. Call the pricing router (`mcp_azure_mcp_pricing`/`azure-pricing`) with `command: "pricing_get"` + a `parameters{}` object inline per paid service. If MCP unavailable or fails β β Read [`subagent-pricing.md`](references/subagent-pricing.md) β dispatch as `task` (NEXT action MUST be `task`, β agent_type: `"task"` β NEVER `"general-purpose"`). Copy the **COMPLETE and UNMODIFIED** template text into the task prompt between `<<<TEMPLATE_START>>>` / `<<<TEMPLATE_END>>>` delimiters β do NOT summarize. Append data (services[], region, budget tier) AFTER the template block. Write results to `prepare-plan.json.costEstimate`. | [pricing-guide.md](references/pricing-guide.md) |
| 7 | **Generate naming** | Centralized naming: suffix, prefix, all resource names | β **You MUST read [naming-patterns.md](references/naming-patterns.md)** |
| 8 | **Determine IaC format** | Existing non-Azure `.tf` β `ask_user` Bicep vs TF, write to `overrides[].iacFormat`. No `.tf` β default Bicep. | (inline) |
| 9 | **Write prepare-plan.json** | Per `PreparePlan` schema. Include postDeployRecommendations, deploymentVariables | β **You MUST read [prepare-schemas.ts](references/prepare-schemas.ts)** for `PreparePlan` schema |
| 10 | **Return summary** | Structured summary for orchestrator approval gate | (inline β 1 line) |
| 11 | **Validate plan** | 4-dimension check: Goal Alignment, WAF Alignment, Dependency Completeness, Deployment Viability. Fix inline on failure, document tradeoffs in `assumptions[]`. | All must pass before writing |
### Step 5 β Post-Quota Validation
> β **NEVER present a region without checking quota first.** Skipping quota validation causes cascading deploy failures and extended healing loops.
> β If plan includes PostgreSQL/MySQL, verify `offerRestrictionsVerified: true` β if false/missing, region is blocked. Do NOT proceed to scaffold with unchecked DB services.
> β **Free β unlimited.** Every compute SKU β including F1, Consumption, and Serverless tiers β has a per-subscription, per-region quota. Do NOT skip quota checks because a SKU is free.
> β After region fallback, update ALL `services[].region` in `prepare-plan.json`. Do not leave stale values.
## Error Handling
| Error | Remediation |
|-------|-------------|
| Pricing API 400 | Verify `--sku` included. Free tiers: skip API |
| MCP pricing unavailable | Dispatch [`subagent-pricing.md`](references/subagent-pricing.md) as `task` fallback (uses direct HTTP to `prices.azure.com`) |
| Prereq output missing | Trigger prereq backfill |
| Quota check fails | Fall back to best-effort estimate + disclaimer |
| Override conflicts | Re-run from Step 3 with new constraints |
deploy-strategy.md 6.8 KB
# Deploy Strategy
Determine how application code will be deployed to Azure based on prereq scan results. The prepare phase writes `deployStrategy` to `prepare-plan.json`; scaffold encodes it in Bicep; deploy executes it.
## Deployment Patterns
Three patterns β select based on `prereq-output.json.components[].buildRequirements`:
> β **Dockerfile β Container Apps.** A Dockerfile that only serves static files (nginx, httpd, `COPY . /usr/share/nginx/html`) is NOT a backend app. Route static-only Dockerfiles as static sites per `service-mapping.md Β§ Static Dockerfile sites`, not Pattern C.
| Pattern | When | `deployStrategy` needed? |
|---------|------|--------------------------|
| **A: Oryx auto-build** | No native modules, no Dockerfile | Yes β startup command + app settings go in Bicep at scaffold time |
| **B: Startup-install** | Native modules detected (`hasNativeModules: true`) | Yes β startup command + fallback install + app settings in Bicep |
| **C: Container-only** | Has Dockerfile that runs a server process (Express, Flask, uvicorn, etc.) | No β route to Container Apps (Dockerfile IS the deploy strategy) |
**Additional routing:**
| Condition | Action |
|-----------|--------|
| Jib build plugin (Java + `com.google.cloud.tools.jib`) | Container-only via Jib push to ACR β no Dockerfile needed |
---
## Pattern A: Oryx Auto-Build (Default)
**Languages:** Node.js, Python, .NET, Go, Java, PHP, Ruby
Oryx detects the stack from project manifests, installs dependencies, and builds automatically during zip deploy.
**Still write `deployStrategy` to `prepare-plan.json`** β even though Oryx auto-detects, the startup command and app settings MUST be in Bicep at scaffold time (not generated at deploy time). This eliminates imperative CLI commands during deploy.
```json
"deployStrategy": {
"codeDeployPattern": "oryx-auto",
"requiredAppSettings": {
"SCM_DO_BUILD_DURING_DEPLOYMENT": "true",
"ENABLE_ORYX_BUILD": "true",
"ORYX_DISABLE_COMPRESSION": "true"
},
"reason": "Standard Oryx build β no native modules, no Dockerfile. Compression disabled to avoid startup extraction delays. No custom appCommandLine β Oryx launcher handles decompression + start."
}
```
Read `prereq-output.json.entryPoint` for the app's start file β do NOT re-read manifests. Build the startup command from `package.json` `start` script (Node.js) or framework convention (Python gunicorn, .NET/Go/Java Oryx-native).
> β **Do NOT set a custom `appCommandLine` for Pattern A.** Let Oryx use `package.json` `start` script or framework defaults natively. A custom `appCommandLine` (`cd /home/site/wwwroot && node {entryPoint}`) replaces the Oryx launcher entirely β the launcher handles `node_modules.tar.gz` decompression, and bypassing it causes `MODULE_NOT_FOUND` crashes. Only set `appCommandLine` when `initCommands[]` has `required: true` entries (migrations).
>
> β **TypeScript projects:** Verify `typescript` + `@types/*` are in `dependencies` (not `devDependencies`) β Oryx production mode skips devDeps, causing `tsc` build failures.
>
> β **When `initCommands[]` has `required: true` entries:** Set `startupCommand` to prepend migrations: `"cd /home/site/wwwroot && {initCommand} && {framework-default-start}"`. Migrations are idempotent β safe on every cold start. Otherwise, omit `startupCommand` entirely (let Oryx handle it).
Scaffold encodes `startupCommand` β Bicep `appCommandLine`, and `requiredAppSettings` β Bicep `siteConfig.appSettings`. Deploy only does: wait β zip β health check.
---
## Pattern B: Startup-Install (Native Modules)
When native modules are detected, Oryx may fail to compile them. The startup-install pattern provides a two-layer safety net.
### Two-Layer Strategy
1. **Primary β Oryx zip build:** `SCM_DO_BUILD_DURING_DEPLOYMENT=true` + `ENABLE_ORYX_BUILD=true` tells Oryx to run dependency installation during the Kudu-side zip deploy. The Kudu build environment on App Service Linux has `gcc`, `make`, and build tools available, so native compilation CAN succeed here.
2. **Fallback β startup-install command:** `appCommandLine` runs dependency installation on first container boot IF the dependency directory doesn't exist. The existence guard ensures it only runs when needed β subsequent restarts skip it because `/home` is persistent storage.
Both layers are set in Bicep at scaffold time. The startup command is insurance β not the primary mechanism.
> **Why two layers?** `az webapp deploy --type zip` uses the OneDeploy API, which may not trigger Oryx even with `SCM_DO_BUILD_DURING_DEPLOYMENT=true`. The startup command catches this case. If Oryx DID build successfully, the guard skips the redundant install.
### Deploy Strategy Schema
Write to `prepare-plan.json.deployStrategy`:
```json
"deployStrategy": {
"codeDeployPattern": "startup-install",
"startupCommand": "cd /home/site/wwwroot && if [ ! -d node_modules ]; then npm install --production; fi && node index.js",
"requiredAppSettings": {
"WEBSITES_CONTAINER_START_TIME_LIMIT": "1800",
"SCM_DO_BUILD_DURING_DEPLOYMENT": "true",
"ENABLE_ORYX_BUILD": "true",
"ORYX_DISABLE_COMPRESSION": "true"
},
"reason": "Native module (better-sqlite3 via node-gyp) requires server-side npm install."
}
```
Replace `startupCommand` and `reason` with language-specific values from the entry point table below.
### Entry Point & Startup Commands
Same as Pattern A, but Node.js adds a dependency guard: `if [ ! -d node_modules ]; then npm install --production; fi` before the start command.
> β **Inline commands only.** Never generate a `.sh` startup script file β CRLF causes `bash` exit code 2.
> β **Python: do NOT use `venv` in startup commands.**
### SKU Implications
When `f1Viable: false` (any of: native modules, TypeScript build, large deps, WSGI/ASGI server), F1 is not viable β use B1 (~$13/mo) minimum. Surface at approval gate: "β οΈ {f1BlockReason}. B1 minimum required."
### Container Timeout
`WEBSITES_CONTAINER_START_TIME_LIMIT` controls how long Azure waits for the container to start responding.
| Value | Use case |
|-------|----------|
| 230 (default) | Standard apps, no native compilation |
| 1800 (max) | Startup-install β native compilation takes 2-5 min. Python with scipy/scikit-learn can take longer |
Always set to `1800` when `codeDeployPattern == "startup-install"`.
---
## Pattern C: Container-Only
When the component has a Dockerfile with backend logic, route to **Container Apps**. The Dockerfile IS the deploy strategy β no `deployStrategy` needed in `prepare-plan.json`.
The deploy phase handles: ACR build β image push β Bicep redeploy with real image. See [code-deployment-container-apps.md](../../deploy/references/code-deployment-container-apps.md).
For Java apps using Jib (`build.gradle` + `com.google.cloud.tools.jib`), the build produces a container image without a Dockerfile β push to ACR via `jib` task.
mcp-tools.md 5.5 KB
# Prepare Phase β MCP Tools
Phase-exclusive tool parameters for the prepare phase. For shared tools (`subscription_list`, `group_list`, `get_azure_bestpractices`, `extension_cli_install`), see [mcp-tool-reference.md](../../references/mcp-tool-reference.md).
> **Troubleshooting:** If a tool call fails with unknown parameter or missing command errors, consult the official docs: <https://learn.microsoft.com/en-us/azure/developer/azure-mcp-server/tools/>
---
## `mcp_azure_mcp_pricing` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `pricing_get` | *(none)* | `service` (service name, e.g. "Azure App Service"), `sku` (`armSkuName` value), `region` (ARM region name), `currency` (e.g. "USD"), `filter` (OData filter for retail prices API) | β
|
**Usage:** Query Azure retail pricing. Use `service` + `region` for broad queries, `sku` for exact SKU lookup (when `armSkuName` is populated β e.g., Redis Cache, App Service Premium). Use `filter` for advanced OData queries against the retail prices API. See [pricing-guide.md](pricing-guide.md) for per-service filter strings and meter name mappings.
## `mcp_azure_mcp_quota` (hierarchical)
> β **Do NOT use for AppOnboard quota checks.** Use `az rest` with the Quota REST API instead β see [sku-quota-validation.md](sku-quota-validation.md). The MCP quota tool returns misleading "No Limit" values for unsupported resource types β "No Limit" means the quota API doesn't support that resource type, NOT unlimited capacity.
| Command | Required Params | Optional Params | Read-Only |
|---------|----------------|-----------------|-----------|
| `quota_usage_check` | `region`, `resource-types` (ARM type, e.g. `Microsoft.Compute/virtualMachines`) | `subscription` | β
|
| `quota_region_availability_list` | `resource-types` (ARM type) | `subscription` | β
|
## `mcp_azure_mcp_cloudarchitect` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `cloudarchitect_design` | *(none)* | `question`, `question-number`, `total-questions`, `answer`, `next-question-needed` (bool), `confidence-score` (0.0β1.0), `state` (JSON β tracks architectureComponents, architectureTiers, requirements{explicit,implicit,assumed}, confidenceFactors) | β
|
**Usage:** Multi-turn conversational tool. For single-shot use, populate `state` with known requirements, set `confidence-score` β₯ 0.7 and `next-question-needed: false` to get a direct recommendation without follow-ups.
## `mcp_azure_mcp_wellarchitectedframework` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `wellarchitectedframework_serviceguide_get` | *(none)* | `service` (case-insensitive, hyphens/underscores/spaces OK, e.g. "cosmos-db", "App Service", "cosmosdb") | β
|
**Usage:** Omit `service` to list all supported services. Provide `service` to get per-service guidance across all 5 WAF pillars.
## `mcp_azure_mcp_advisor` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `advisor_recommendation_list` | *(none)* | `subscription`, `resource-group` | β
|
## `mcp_azure_mcp_group_resource_list`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| `resource-group` | `subscription`, `tenant` | β
|
Returns: resource names, IDs, types, locations within the group.
## `mcp_azure_mcp_policy` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| *(subscription scope)* | *(none)* | `subscription` | β
|
**Usage:** Bulk fetch subscription policy constraints β blocked resource types, required tags, allowed regions. Use in prepare Step 2 alongside `advisor_recommendation_list` to surface governance constraints early.
---
## Phase 2 Tool Map
| Tool | Sub-command | AppOnboard Step | Purpose |
|------|-----------|----------|---------|
| `mcp_azure_mcp_cloudarchitect` | `cloudarchitect_design` | Step 3 | Architecture validation. Single-shot: populate `state` from `context.json`, set `confidence-score: 0.8` |
| `mcp_azure_mcp_wellarchitectedframework` | `wellarchitectedframework_serviceguide_get` | Step 4 | Per-service WAF guidance. Call with `service: "{service-name}"` for each planned service |
| `mcp_azure_mcp_advisor` | `advisor_recommendation_list` | Step 2 | Get existing subscription recommendations alongside policy query |
| `mcp_azure_mcp_policy` | *(hierarchical)* | Step 2 | Subscription policy constraints β blocked types, required tags, allowed regions |
| `mcp_azure_mcp_pricing` | `pricing_get` | Step 6 | Cost estimation. Read [pricing-guide.md](pricing-guide.md) first |
| `mcp_azure_mcp_quota` | β Do NOT use | Step 5 | β Read [sku-quota-validation.md](sku-quota-validation.md) and use `az rest` with Quota REST API instead |
| `mcp_azure_mcp_subscription_list` | *(flat)* | Step 1 | Resolve target subscription if not in context |
| `mcp_azure_mcp_group_list` | *(flat)* | Step 7 | List existing RGs for reuse/conflict detection |
| `mcp_azure_mcp_group_resource_list` | *(flat)* | Step 7 | Detect naming collisions in target RG |
---
## Tool Pitfalls
- **`mcp_azure_mcp_pricing` β `pricing_get`:** Use `--sku` when querying by `armSkuName` (e.g., Redis Cache, App Service Premium). For services without `armSkuName`, use `filter` and `meterName` matching instead. Omitting all filter params returns too many results.
naming-patterns.md 4.6 KB
# Naming Patterns
Per-resource naming rules for AppOnboard-generated Azure resources. Apply in Step 7 (Generate naming).
> **Reference:** [Azure naming conventions](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-naming) β link, don't duplicate. Check docs for updates.
## Default Pattern
```
{abbreviation}-{resourcePrefix}
```
Where `resourcePrefix` = `{project}-{env}-{suffix}` (generated once in prepare Step 7, stored in `prepare-plan.json.naming.resourcePrefix`).
- `{project}` β app name from `context.json.app.name` (lowercase, alphanumeric + hyphens)
- `{env}` β `dev` / `staging` / `prod` (from intent or default `dev`)
- `{suffix}` β first 4 chars of session UUID (e.g., `a1d5`). Generated once, stored in `naming.suffix`
- `{abbreviation}` β short service prefix from the table below (e.g., `app`, `kv`, `rg`)
**Examples with `resourcePrefix = myapp-dev-a1d5`:**
- Resource Group: `rg-myapp-dev-a1d5`
- App Service: `app-myapp-dev-a1d5`
- Key Vault: `kv-myapp-dev-a1d5`
- Storage: `stmyappdeva1d5` (alphanumeric only)
> β **No redundancy.** The resource name is `{abbr}-{resourcePrefix}` β NOT `{project}-{abbr}-{project}-{env}-{suffix}`. The project name appears ONCE in the prefix.
>
> β **Scaffold reads naming from the plan β does NOT generate or modify resource names.**
Override via `context.json.overrides[]` with `key: "naming.pattern"`.
## Per-Resource Rules
| Resource | Abbreviation | Max Length | Allowed Chars | Globally Unique? | Example |
|----------|-------------|------------|---------------|------------------|---------|
| Resource Group | `rg` | 90 | Alphanumeric, hyphens, underscores, periods, parens | No (but include suffix to avoid cross-session collisions) | `rg-myapp-dev-a1d5` |
| App Service | `app` | 60 | Alphanumeric, hyphens | **Yes** | `app-myapp-dev-a1d5` |
| Container App | `ca` | 32 | Lowercase alphanumeric, hyphens | No (within env) | `ca-myapp-dev-a1d5` |
| Container Registry | `cr` | 50 | **Alphanumeric only** | **Yes** | `crmyappdeva1d5` |
| Azure SQL Server | `sql` | 63 | Lowercase alphanumeric, hyphens | **Yes** | `sql-myapp-dev-a1d5` |
| Cosmos DB | `cosmos` | 44 | Lowercase alphanumeric, hyphens | **Yes** | `cosmos-myapp-dev-a1d5` |
| Storage Account | `st` | 24 | **Lowercase alphanumeric only** | **Yes** | `stmyappdeva1d5` |
| Key Vault | `kv` | 24 | Alphanumeric, hyphens | **Yes** | `kv-myapp-dev-a1d5` |
| Log Analytics | `log` | 63 | Alphanumeric, hyphens | No (within RG) | `log-myapp-dev-a1d5` |
| App Insights | `appi` | 260 | Most chars | No (within RG) | `appi-myapp-dev-a1d5` |
| Service Bus | `sb` | 50 | Alphanumeric, hyphens | **Yes** | `sb-myapp-dev-a1d5` |
| Functions | `func` | 60 | Alphanumeric, hyphens | **Yes** | `func-myapp-dev-a1d5` |
| Static Web Apps | `swa` | 40 | Alphanumeric, hyphens | No (suffix still required β Rule 1) | `swa-myapp-dev-a1d5` |
| Redis Cache | `redis` | 63 | Alphanumeric, hyphens | **Yes** | `redis-myapp-dev-a1d5` |
> **CAF deviation:** Azure Cloud Adoption Framework uses `sbns` for Service Bus namespace. AppOnboard uses `sb` for brevity β either is acceptable.
## Rules
1. **ALL resources get the `{suffix}`** β including resource groups. This prevents cross-session naming collisions when the same app is deployed multiple times. The suffix is a 4-char random string generated once per session.
2. **Container Registry + Storage Account:** strip hyphens (alphanumeric only). β **Mechanical transform β do NOT re-derive char-by-char:** `('cr' + resourcePrefix).replace(/-/g,'').toLowerCase()`, truncate to β€50 (ACR) / β€24 (Storage). Worked example: `resourcePrefix = bezkoder-dev-18a3` β `cr` + `bezkoderdev18a3` β `crbezkoderdev18a3`. Copy the pattern; do not spell out the concatenation.
3. **Validate length** after substitution β Key Vault (24 chars) is the tightest constraint. Budget for `{project}`: `24 - len(abbreviation) - len(env) - len(suffix) - 3 - 2` (separators + healing reserve). For `kv` + `dev`: 10 chars max. The 2-char reserve ensures room for a healing suffix (e.g., `edd6` β `edd602`) on region fallback. **Truncation when over budget:**
1. Truncate at the nearest hyphen boundary within budget (e.g., `broken-web-app` at 10 β `broken-web`)
2. If no hyphen within budget: hard truncate, no trailing hyphen
3. Verify truncated name doesn't collide with reserved words (Rule 4)
4. Recompute ALL resource names with the truncated project β do NOT mix long and short names
4. **Never use reserved words** as resource names (`admin`, `login`, `root`, `test`)
5. Populate `naming.resources[]` in `prepare-plan.json` with concrete names after validation
prepare-schemas.ts 6.7 KB
/**
* Prepare artifact schema β prepare-plan.json.
* Read by prepare SKILL.md Step 9 (write prepare-plan.json).
*/
// βββ Shared type (inlined from session-schemas.ts to avoid cross-ref) ββββββββ
export interface PostDeployRecommendation {
title: string;
reason: string;
effort: "low" | "medium" | "high";
services?: string[];
}
// βββ Healing types (prepare phase) βββββββββββββββββββββββββββββββββββββββββββ
export type PrepareHealingTrigger = "quota" | "policy" | "rubric" | "region";
export type PrepareIssueClassification = "AUTO_FIXABLE" | "NEEDS_USER_INPUT";
export type PrepareHealingResult = "fixed" | "needs-input" | "still-failing";
export interface PrepareHealingIssue {
dimension: string;
detail: string;
classification: PrepareIssueClassification;
}
export interface PrepareHealingFix {
service: string;
change: string;
reason: string;
}
export interface PrepareHealingAttempt {
attempt: number;
trigger: PrepareHealingTrigger;
issues: PrepareHealingIssue[];
fixes: PrepareHealingFix[];
result: PrepareHealingResult;
}
// βββ prepare-plan.json βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export interface PlannedService {
name: string;
sku: string;
purpose: string;
component: string;
region: string;
resourceName: string;
/** Exact engine version for managed database services (MySQL/PostgreSQL Flexible Server),
* sourced from the provider capabilities API during quota validation β NOT guessed.
* ARM rejects major-only strings (e.g. MySQL '8.0'); use the exact supported patch
* (e.g. '8.0.21'). Omit for non-DB services. */
version?: string;
}
export interface CostBreakdownItem {
service: string;
sku: string;
monthlyUsd: number;
note?: string;
}
export interface CostEstimate {
monthlyUsd: number;
currency: "USD";
breakdown: CostBreakdownItem[];
disclaimer: string;
}
export interface RejectedAlternative {
service: string;
reason: string;
}
export interface NamingResource {
type: string;
name: string;
}
export interface NamingConfig {
pattern: string;
/** The computed prefix used for all resource names: {project}-{env}-{suffix}.
* deploymentVariables.environmentName MUST equal this value. */
resourcePrefix: string;
resources: NamingResource[];
}
export type QuotaSource = "cli" | "fallback";
export interface QuotaCheck {
resource: string;
region: string;
required: number;
available: number;
sufficient: boolean;
provider: string;
currentUsage: number;
currentLimit: number;
adjustable: boolean;
source: QuotaSource;
}
export interface DeployStrategy {
/** Deploy pattern β see deploy-strategy.md for the three patterns.
* "oryx-auto": Oryx handles build + run (Pattern A β default for most apps).
* "startup-install": native module compilation at startup (Pattern B).
* "container-only": Dockerfile-based deploy via ACR (Pattern C).
* Omit when Oryx auto-build is sufficient and no special startup is needed. */
codeDeployPattern?: "oryx-auto" | "startup-install" | "container-only";
/** Startup command for native module compilation (goes into appCommandLine) */
startupCommand?: string;
/** Required app settings for the deploy strategy (e.g., SCM_DO_BUILD_DURING_DEPLOYMENT) */
requiredAppSettings?: Record<string, string>;
/** Human-readable reason for choosing this strategy */
reason: string;
}
export interface InstrumentationConfig {
appInsightsEnabled: boolean;
reason: string;
}
export interface DeploymentVariables {
/** MUST equal naming.resourcePrefix (e.g., "myapp-dev-a1d5"), NOT the env label ("dev") */
environmentName: string;
location: string;
sessionId: string;
deployedBy: string;
/** Variable names that could not be resolved at prepare time β deploy reads, not asks */
deferred?: string[];
}
export interface OfferRestriction {
/** Azure provider namespace (e.g., "Microsoft.DBforPostgreSQL") */
provider: string;
/** Region checked */
region: string;
/** Whether the offer is restricted in this region */
restricted: boolean;
/** Reason string from capabilities API, if any */
reason?: string;
}
export interface QuotaValidation {
/** True when all planned services had quota confirmed */
verified: boolean;
/** How quota was verified β persists in session artifact, survives context compaction */
method?: "cli" | "what-if" | "unverifiable";
/** The specific region that passed validation */
verifiedRegion?: string;
/** The specific SKU that was validated (e.g., "B1", "F1") */
verifiedSku?: string;
/** Regions checked during validation */
checkedRegions?: string[];
/** Resources that failed quota checks */
failedResources?: string[];
/** Reason when verified is false (e.g., "quota CLI unavailable", "all regions zero") */
reason?: string;
/** Offer restriction results for database services (PostgreSQL, MySQL).
* Populated by prepare Step 5 when database services are in the plan.
* `LocationIsOfferRestricted` is NOT caught by what-if β this is the only pre-deploy check. */
offerRestrictions?: readonly OfferRestriction[];
/** Derived from offerRestrictions[]: true when every database service in services[]
* has at least one entry in offerRestrictions[]. Set this based on the array contents,
* not independently. Deploy gate halts if false/missing when services include PostgreSQL/MySQL. */
offerRestrictionsVerified?: boolean;
}
export interface PreparePlan {
services: PlannedService[];
costEstimate: CostEstimate;
rejectedAlternatives: RejectedAlternative[];
naming: NamingConfig;
quotas: readonly QuotaCheck[];
assumptions: string[];
/** IaC format for scaffold β "bicep" (default) or "terraform" */
iacFormat: "bicep" | "terraform";
/** Application database name the app expects to exist (from compose env such as
* MYSQL_DATABASE / MYSQLDB_DATABASE / POSTGRES_DB, a connection string, or ORM config).
* Scaffold emits a `flexibleServers/databases` child resource for this so the schema DB
* exists in IaC before the container starts; the conformance gate's DB-NAME-PRESENT check
* asserts it. Omit only when no managed database is in the plan. */
appDbName?: string;
postDeployRecommendations?: PostDeployRecommendation[];
/** Instrumentation decision β object with boolean + reason, NOT a bare boolean */
instrumentation?: InstrumentationConfig;
/** Deployment variables resolved at prepare time β scaffold/deploy read, not re-ask */
deploymentVariables?: DeploymentVariables;
/** Quota validation result from proactive capacity check */
quotaValidation?: QuotaValidation;
/** Code deploy strategy for native module apps β see deploy-strategy.md.
* Only populated when Pattern B (startup-install) is needed. */
deployStrategy?: DeployStrategy;
healingAttempts?: readonly PrepareHealingAttempt[];
}
pricing-guide-services.md 8.4 KB
# App Onboard Prepare β Per-Service Pricing Patterns
Filter strings, meter names, and formulas per Azure service. For methodology and troubleshooting, see [pricing-guide.md](pricing-guide.md).
### Container Apps
No `armSkuName`. Filter: `serviceName eq 'Azure Container Apps' and armRegionName eq '{region}' and priceType eq 'Consumption' and isPrimaryMeterRegion eq true`
**Key meters:** `Standard vCPU Active Usage` (1 Second), `Standard Memory Active Usage` (1 GiB Second), `Standard Requests` (1M).
**Monthly (Consumption, 1 vCPU, 2 GiB, 8h active/day):**
`(vCPU_active_rate Γ 3600 Γ 8 Γ 30) + (memory_active_rate Γ 2 Γ 3600 Γ 8 Γ 30)`
**Free grant:** 180K vCPU-sec + 360K GiB-sec + 2M requests/sub/month. Scale-to-zero = $0 idle. When `min_replicas >= 1`: add idle cost from idle meters.
---
### Container Registry (ACR)
Empty `armSkuName` β β do NOT filter by `sku` (returns `[]`). Filter: `serviceName eq 'Container Registry' and armRegionName eq '{region}' and priceType eq 'Consumption'`, then match `meterName`.
| Tier | `meterName` | `unitOfMeasure` | Monthly |
|------|-------------|-----------------|---------|
| Basic | `Basic Registry Unit` | `1/Day` | `retailPrice Γ 30` (~$5) |
| Standard | `Standard Registry Unit` | `1/Day` | `retailPrice Γ 30` (~$20) |
| Premium | `Premium Registry Unit` | `1/Day` | `retailPrice Γ 30` (~$50) |
β **`1/Day` β Γ 30, NOT Γ 730.** The other ACR meters (`Task vCPU Duration` = `1 Second` build compute, `Data Stored` = `1 GB/Month`) are usage-based β exclude from the fixed monthly unless the app builds heavily. ACR Basic fixed cost β **$5/mo**.
---
### App Service
**MCP call:** `pricing_get --service "Azure App Service" --region "{region}"`
**Meter-to-SKU mapping:**
| SKU | `meterName` | Notes |
|-----|-------------|-------|
| F1 | `F1 App` | Free ($0 price) |
| B1 | `B1` | |
| B2 | `B2` | |
| S1 | `S1 App` | |
| P1v3 | `P1 v3 App` | Linux/Windows have different `productName` |
**Gotcha:** `armSkuName` empty for Basic/Standard. Match on `meterName`.
**Monthly:** `retailPrice Γ 730`
---
### Azure SQL Database
**DTU model** β no `armSkuName`. Filter by `productName` + `meterName`.
| SKU | `productName` contains | `meterName` (unit: 1/Day) |
|-----|----------------------|-------------|
| Basic (5 DTU) | `SQL Database Single Basic` | `B DTU` |
| S0 (10 DTU) | `SQL Database Single Standard` | `S0 DTUs` |
| S1 (20 DTU) | `SQL Database Single Standard` | `S1 DTUs` |
**Monthly (DTU):** `retailPrice Γ 30`
**vCore model:** `productName` filters β Serverless: `'SQL Database General Purpose - Serverless - Compute Gen5'`. Hyperscale: `'SQL Database Single/Elastic Pool Hyperscale'`. Monthly: `retailPrice Γ 730`.
---
### Cosmos DB
Filter: `serviceName eq 'Azure Cosmos DB' and armRegionName eq '{region}' and priceType eq 'Consumption' and isPrimaryMeterRegion eq true`
| Model | Match on | Monthly formula |
|-------|----------|----------------|
| Provisioned | `meterName eq '100 RU/s'`, `productName eq 'Azure Cosmos DB'` | `(targetRU / 100) Γ retailPrice Γ 730` |
| Autoscale | `productName eq 'Azure Cosmos DB autoscale'`, `meterName` starts with `AP` | varies |
| Serverless | `productName eq 'Azure Cosmos DB serverless'`, `meterName eq '1M RUs'` | `retailPrice Γ estimatedMillionsOfRU` |
> β οΈ **Provisioned 100 RU/s gotcha:** The `isPrimaryMeterRegion eq true` entry has `retailPrice: 0` (free-tier placeholder). The real price (~$0.008/hr in eastus) has `isPrimaryMeterRegion: false`. Drop `isPrimaryMeterRegion` from the filter or match on `retailPrice > 0`.
**Storage:** `meterName eq 'Data Stored'` with `productName eq 'Azure Cosmos DB'` (1 GB/Month). Note: this meter has `isPrimaryMeterRegion: false` β drop that filter or query without it.
**Free tier:** 1,000 RU/s + 25 GB per account (one per subscription).
---
### Storage
No `armSkuName`. Filter by `meterName` + `productName`.
**Filter (Hot LRS Blob):**
```
serviceName eq 'Storage' and armRegionName eq '{region}' and meterName eq 'Hot LRS Data Stored' and productName eq 'Blob Storage'
```
**Meter-to-SKU mapping:**
| SKU matrix tier | `meterName` | `productName` |
|----------------|-------------|---------------|
| Standard_LRS | `Hot LRS Data Stored` | `Blob Storage` or `General Block Blob v2` |
| Standard_GRS | `Hot GRS Data Stored` | `Blob Storage` or `General Block Blob v2` |
| Premium_LRS | `Premium LRS Data Stored` | `Premium Block Blob` |
**Monthly:** `retailPrice Γ estimatedGB`
---
### Key Vault
**Filter:**
```
serviceName eq 'Key Vault' and armRegionName eq '{region}' and priceType eq 'Consumption'
```
**Key meters:**
| Tier | `meterName` | Unit |
|------|-------------|------|
| Standard | `Operations` | 10K |
**Monthly (Standard):** `retailPrice Γ (estimatedOps / 10000)`. Negligible for typical AppOnboard apps.
---
### Service Bus
**Filter:**
```
serviceName eq 'Service Bus' and armRegionName eq '{region}'
```
> β οΈ **Do NOT add `isPrimaryMeterRegion eq true`** β all Service Bus meters have `isPrimaryMeterRegion: false`. Adding that filter returns zero useful results.
**Key meters:**
| Tier | `meterName` | Unit | Notes |
|------|-------------|------|-------|
| Basic | `Basic Messaging Operations` | 1M | Usage-based only |
| Standard | `Standard Base Unit` | 1/Hour | Base cost (always-on) |
| Standard | `Standard Messaging Operations` | 1M | Tiered: first 13M free |
| Premium | `Premium Messaging Unit` | 1/Hour | Per messaging unit |
**Monthly (Standard):** `baseUnitRate Γ 730` + operations beyond free tier. Standard tiered: first 13M free.
**Gotcha:** Standard Base Unit has two entries β hourly and monthly. Use hourly Γ 730.
---
### Redis Cache
`armSkuName` IS populated β `pricing_get --sku` works.
**MCP call:** `pricing_get --service "Redis Cache" --sku "{armSkuName}" --region "{region}"`
**armSkuName pattern:** `Azure_Redis_Cache_{Tier}_{Size}_Cache` (e.g., `Azure_Redis_Cache_Basic_C0_Cache`, `Azure_Redis_Cache_Standard_C1_Cache`). Query the MCP tool for exact pricing.
**Gotcha:** Basic tier entries have `isPrimaryMeterRegion: false`. The standard filter strips them out. Query by `armSkuName` directly or omit `isPrimaryMeterRegion` and match on `productName eq 'Azure Redis Cache Basic'`.
**Monthly:** `retailPrice Γ 730`
---
### Azure Database for PostgreSQL Flexible Server
`armSkuName` IS populated β `pricing_get --sku` works. β **`skuName` is CASE-SENSITIVE:** `B1MS` works, `B1ms` returns 0 results.
**MCP call:** `pricing_get --service "Azure Database for PostgreSQL" --sku "{armSkuName}" --region "{region}"`
**armSkuName values:** β Case-sensitive. Small Burstable uses short names (`B1MS`, `B2S`); larger Burstable + GP/MO use `Standard_` prefix.
| SKU | `armSkuName` |
|-----|-------------|
| Burstable B1ms | `B1MS` |
| Burstable B2s | `B2S` |
| Burstable B2ms | `Standard_B2ms` |
| General Purpose D2ds_v5 | `Standard_D2ds_v5` |
**Storage:** Query separately β `meterName eq 'Storage Data Stored'` and `productName` containing `Flexible Server Storage`. Unit: 1 GiB/Month. Default 32 GiB included at ~$0.115/GiB/mo.
**Monthly (compute):** `retailPrice Γ 730`
**Monthly (storage):** `storageRate Γ storageSizeGB`
**Total:** compute + storage (e.g., B1ms + 32 GiB β $12.41 + $3.68 = ~$16.09)
---
### Functions
**Consumption plan:** Not in API. Free: 1M executions + 400K GB-seconds/month.
**Flex/Premium** β filter: `serviceName eq 'Functions' and armRegionName eq '{region}' and priceType eq 'Consumption' and isPrimaryMeterRegion eq true`
- Flex: `On Demand Execution Time` (1 GB Second), `On Demand Total Executions` (10), `Always Ready Baseline`
- Premium: `Premium vCPU Duration` (1 Hour), `Premium Memory Duration` (1 GiB Hour). Monthly: `vCPU_rate Γ 730 + memory_rate Γ GiB Γ 730`
---
### Static Web Apps
**Not in the Retail Prices API.** Fixed pricing β query [azure.microsoft.com/pricing/details/app-service/static](https://azure.microsoft.com/en-us/pricing/details/app-service/static/):
- **Free tier:** 100 GB bandwidth, 2 custom domains
- **Standard tier:** Flat monthly per app, unlimited bandwidth, 5 custom domains
---
### Log Analytics / Application Insights
**Filter:**
```
serviceName eq 'Azure Monitor' and armRegionName eq '{region}' and priceType eq 'Consumption' and isPrimaryMeterRegion eq true
```
**Key meters:**
- `Platform Logs Data Processed` (unit: 1 GB)
- Log Analytics ingestion β match `meterName` containing `Data Ingestion`
**Free grant:** First 5 GB/month of Log Analytics data ingestion.
**Monthly:** `retailPrice Γ estimatedGB` (subtract 5 GB free grant).
pricing-guide.md 3.9 KB
# App Onboard Prepare β Pricing Guide
> β **SELF-CHECK:** If `costEstimate.breakdown[]` has ANY `monthlyUsd > 0`, at least one `pricing_get` or `az rest` pricing API call MUST appear in this session. Estimating from memory or training data is NEVER acceptable β reference values in this file may be outdated. If API fails after 2 attempts, use the reference value WITH disclaimer: `"β οΈ Estimated from reference β live API unavailable."`
## Free-Tier Shortcut β Skip API
> β **Check this FIRST.** If ALL services use free-tier SKUs β write $0 cost estimate, add disclaimer "Estimate assumes usage within free grant limits", skip to Step 7.
| Service | Free SKU | Skip API? |
|---------|----------|-----------|
| App Service | F1 | Yes |
| Static Web Apps | Free | Yes |
| Functions | Consumption (β€1M exec) | Yes |
| Cosmos DB | Free tier (1000 RU/s) | Yes |
| Container Apps | Consumption (β€180K vCPU-s) | Yes |
---
## App Service Quick Reference
| SKU | `skuName` | Monthly |
|-----|-----------|---------|
| B1 Linux | `B1` | ~$13.14 |
> β **Case-sensitive:** use `B1` not `b1`. Use `skuName` (not `armSkuName`) for Basic/Standard.
---
## How to Use
1. Pick SKU from [sku-matrix.md](sku-matrix.md) based on budget intent.
2. Find service section in [pricing-guide-services.md](pricing-guide-services.md) for filter strings and formulas.
3. Call the pricing router β tool `mcp_azure_mcp_pricing` (VS Code) / `azure-pricing` (CLI) β with `intent`, `command: "pricing_get"`, and a `parameters` object (NOT `--flags`). Parallel OK. β At least one filter inside `parameters`: `sku`, `service`, `region`, `service-family`, or `filter`. β **`sku` matches `armSkuName`, not `skuName`** β use it ONLY when `armSkuName` is populated (App Service, MySQL/PostgreSQL, Redis). Empty-`armSkuName` services (ACR, Storage, Cosmos, Key Vault) return `[]` or 400 for `sku`/`service` β use a raw `filter` on `serviceName` + `meterName` instead.
4. Apply the monthly multiplier from each meter's `unitOfMeasure` (see Β§ Monthly Multiplier) β NOT a per-service constant.
5. Cross-check returned `retailPrice` against planned SKU.
## Monthly Multiplier β by Meter Unit
β **Read `unitOfMeasure` per price record β NEVER blanket `Γ 730`** (one service mixes units, e.g. ACR returns `1/Day` + `1 Second` + `1 GB/Month`):
| `unitOfMeasure` | Monthly formula |
|-----------------|-----------------|
| `1 Hour` / `1/Hour` | `retailPrice Γ 730` |
| `1/Day` / `1 Day` | `retailPrice Γ 30` |
| `1 GB/Month` | `retailPrice Γ estimatedGB` (already monthly) |
| `1 Second` / `1 GiB Second` | `retailPrice Γ activeUnitsPerMonth` (usage-based) |
| `10K` / `1M` operations | `retailPrice Γ estimatedOps Γ· unitSize` |
## Usage-Based Services
When AI/OpenAI/LLM components detected: add `"πΈ Usage-based β excluded from total"` to `costEstimate.breakdown[]` and `"β οΈπ€ AI inference costs excluded"` to `assumptions[]`. Surface at EVERY approval gate.
## Service Pricing Reference
See [pricing-guide-services.md](pricing-guide-services.md) for per-service filter strings, meter names, and monthly formulas.
**Key rules:**
- `armSkuName` is empty for most PaaS β filter by `meterName` or `productName`
- Monthly multiplier: read `unitOfMeasure` (Β§ Monthly Multiplier) β never assume Γ730
- Functions Consumption / Static Web Apps: not in API
- Default `isPrimaryMeterRegion eq true` + `priceType eq 'Consumption'` β but check the service section in [pricing-guide-services.md](pricing-guide-services.md) first; some drop `isPrimaryMeterRegion` (Service Bus, Cosmos 100 RU/s, Redis Basic).
- Service names are case-sensitive
## Troubleshooting
When `pricing_get` returns empty: (1) Check `armSkuName` populated/case-sensitive? (2) HTTP fallback: `https://prices.azure.com/api/retail/prices?$filter={filter}`. (3) After 2 failures, use [pricing-guide-services.md](pricing-guide-services.md) with disclaimer. (4) Log to `costEstimate.apiFailures[]`.
> β **Never claim user has free credits remaining.** Use: "Check balance at portal."
service-mapping.md 7.9 KB
# Service Mapping Tables
ComponentβAzure service selection. Apply `context.json.intent` as modifiers, `context.json.overrides[]` as hard constraints, and policy constraints as filters.
## Hosting
> β **Implicit dependencies:** When selecting Container Apps and the component has a Dockerfile (or `hasDockerfile: true` in prereq), **ALWAYS include Container Registry (Basic)** in `services[]`. Container Apps requires ACR to host custom images β omitting it forces an imperative add during deploy, wasting a healing round. ACR Basic is $0.17/day (~$5/mo).
| Component Type | Primary Service | Alternatives | Selection Signal |
|---------------|----------------|-------------|-----------------|
| SPA Frontend | Static Web Apps | Blob + CDN | React/Vue/Angular, no SSR |
| SSR Web App | Container Apps | App Service, AKS | Next.js/Nuxt, server-rendered |
| REST/GraphQL API | Container Apps | App Service, Functions, AKS | Express/Fastify/Flask/FastAPI |
| Background Worker | Container Apps (scale-to-zero) | Functions, AKS | Celery/Bull/Agenda, no HTTP |
| Scheduled Task | Functions (Timer) | Container Apps Jobs | Cron patterns, periodic execution |
| Event Processor | Functions | Container Apps + KEDA | Event-driven, queue/topic consumer |
| Microservices (K8s) | AKS | Container Apps | kubectl/helm in repo, CRDs, service mesh |
| GPU/ML Workloads | AKS | Azure ML | GPU requirements, training workloads |
**Stack shortcuts:** Containers (Docker, microservices) β Container Apps or AKS. Serverless (event-driven, variable traffic) β Functions. Traditional web (PaaS preference) β App Service.
**AKS vs Container Apps:** Use Container Apps when scale-to-zero needed, no K8s expertise, or KEDA-driven event processing. Delegate AKS planning to `azure-kubernetes` skill.
**App Service vs Container Apps:** App Service preferred when user wants free tier (F1 $0 / B1 ~$13/mo) or single-process with no container orchestration. Container Apps preferred for REST/GraphQL APIs (scaffold generates Dockerfile if missing), scale-to-zero, multi-container/sidecar, or event-driven KEDA scaling. Budget affects SKU tier (Consumption vs Dedicated), not compute service type. Container Apps Consumption has no fixed free tier but scales to zero.
**Static Dockerfile sites (nginx/httpd serving HTML):**
- **Primary:** Static Web Apps Free β $0, global CDN.
> β **SWA region availability:** Validate via `az provider show --namespace Microsoft.Web --query "resourceTypes[?resourceType=='staticSites'].locations" -o tsv`.
- **Alternative:** App Service F1 (Free) β β F1 does NOT run Docker. Use Windows F1 (IIS serves `index.html` natively) or Linux F1 (`linuxFxVersion: 'STATICSITE|1.0'`). β Do NOT use `NODE|*`/`PHP|*`/`PYTHON|*` for static sites β causes 504/503 cold start.
- **If Docker required:** Container Apps (scale-to-zero) or App Service B1+ (custom containers).
**Plain HTML (no package manager, no Dockerfile):** Windows App Service F1 preferred (IIS serves natively). Linux: `linuxFxVersion: 'STATICSITE|1.0'`. Or Static Web Apps Free.
## Data
| Need | Primary Service | Alternatives | Selection Signal |
|------|----------------|-------------|-----------------|
| Relational | Azure SQL | PostgreSQL Flexible, MySQL Flexible | SQL schema, transactions, joins |
| Document/NoSQL | Cosmos DB | β | JSON docs, global distribution |
| Graph (Gremlin) | Cosmos DB (Gremlin API) | β | Graph traversal, relationships |
> **Cosmos DB Serverless:** Each Serverless account supports ONE API type (SQL, MongoDB, Gremlin, Table, Cassandra). Multi-API apps require separate Serverless accounts.
| Cache | Redis Cache | β | Session store, rate limiting |
| Files/Blobs | Blob Storage | Files Storage | File uploads, static assets |
| Search | AI Search | β | Full-text search requirements |
| MariaDB/MySQL | MySQL Flexible Server | β | `mariadb:*` or `mysql:*` in compose / connection config |
| Elasticsearch/OpenSearch | AI Search | Azure Monitor (for Kibana) | `elasticsearch:*` or `opensearch:*` in compose |
| S3-compatible / MinIO | Blob Storage | β | `minio/*` in compose, S3 SDK usage |
## Integration
| Need | Primary Service | Selection Signal |
|------|----------------|-----------------|
| Message Queue | Service Bus | Point-to-point, ordered, transactions |
| Pub/Sub | Event Grid | Event routing, fan-out |
| Streaming / Kafka | Event Hubs | High-throughput telemetry, logs. **Kafka protocol compatible** β apps using `kafka-clients`, Spring Kafka, `confluent-kafka-python` connect by changing bootstrap URL + SASL config only |
| Multi-step orchestration | Durable Functions + Durable Task Scheduler | DTS is the recommended managed backend |
| Low-code workflow | Logic Apps | Integration-heavy, visual designer |
> **docker-compose β Azure mapping:** Map each `detectedServices[].type` to its PaaS equivalent using these tables. **Unmapped services** (SSH daemons, custom sidecars β no managed equivalent): offer a Linux VM (B1s ~$7/mo) as companion at the scaffold gate. Present as option, not forced. Write to `postDeployRecommendations[]`.
## Supporting (always include)
| Service | Purpose |
|---------|---------|
| Log Analytics | Centralized logging |
| Application Insights | Monitoring + APM |
| Key Vault | Secrets management |
| Managed Identity | Service-to-service auth (zero secrets) |
## Specialized Routing
Check before mapping β delegate to specialized skill if matched:
| Signal | Delegate To |
|--------|-------------|
| Copilot SDK, `@github/copilot-sdk` | `azure-hosted-copilot-sdk` |
| Foundry agent, AI agent deployment | `microsoft-foundry` |
> β **Non-Azure cloud SDK deps** (AWS/GCP/Firebase) are handled by prereq β the pipeline does NOT reach prepare with cloud SDK findings present. If you see cloud SDK findings here, prereq failed to stop β HALT and do NOT continue architecture planning.
## Non-Azure Terraform Resource Mapping
When `context.json.detectedInfraProvider.terraform` is `"gcp"` or `"aws"`, read existing `.tf` files and map cloud resources to Azure equivalents. These mappings provide architecture signal for service selection β use alongside component detection.
### GCP β Azure
| GCP Terraform Resource | Azure Equivalent | Notes |
|------------------------|-----------------|-------|
| `google_cloud_run_v2_service` | Container Apps | Map scaling, env vars, VPC config |
| `google_sql_database_instance` (POSTGRES) | PostgreSQL Flexible Server | Map tier, backup, maintenance config |
| `google_sql_database_instance` (MYSQL) | MySQL Flexible Server | Map tier, backup config |
| `google_artifact_registry_repository` | Container Registry (ACR) | Basic tier unless geo-replication needed |
| `google_pubsub_topic` / `google_pubsub_subscription` | Service Bus | Map topic/subscription model |
| `google_secret_manager_secret` | Key Vault | Map secret references |
| `google_firestore_database` | Cosmos DB (NoSQL API) | Map indexes, TTL config |
| `google_cloudfunctions2_function` | Azure Functions | Map triggers, runtime |
| `google_service_account` + `google_project_iam_member` | Managed Identity + RBAC | Map role bindings |
Other GCP resources (storage, redis, compute network, VPC connector, cloud tasks) map 1:1 to their Azure equivalents (Blob Storage, Redis Cache, VNet, Queue Storage).
### AWS β Azure
| AWS Terraform Resource | Azure Equivalent | Notes |
|------------------------|-----------------|-------|
| `aws_ecs_service` / `aws_ecs_task_definition` | Container Apps | Map task def β container config |
| `aws_rds_instance` (postgres/mysql) | PostgreSQL/MySQL Flexible Server | Map instance class β SKU |
| `aws_lambda_function` | Azure Functions | Map runtime, handler, triggers |
| `aws_dynamodb_table` | Cosmos DB (NoSQL API) | Map capacity, indexes |
| `aws_sns_topic` | Service Bus or Event Grid | Map subscriptions |
| `aws_secretsmanager_secret` | Key Vault | Map secret references |
Other AWS resources (S3, ECR, SQS, ElastiCache) map 1:1 to their Azure equivalents (Blob Storage, ACR, Queue Storage/Service Bus, Redis Cache). SQS β Service Bus if FIFO ordering needed.
sku-matrix.md 3.2 KB
# SKU Selection Matrix
Select SKU based on `context.json.intent.budget`. Cross-reference with `intent.scale` for right-sizing.
## Budget Tiers
> β οΈ **F1 (Free) limitations:** 1 GB RAM, shared compute (60 min/day CPU), no custom domain, no always-on, no deployment slots, no VNet integration. Only suitable for low-traffic dev/test. Production apps β B1 minimum.
| Service | cost-optimized | balanced | performance |
|---------|---------------|----------|-------------|
| Container Apps | Consumption (scale-to-zero) | Consumption | Dedicated |
| App Service | F1 (Free) | B1 / S1 | P1v3 |
| Azure SQL | Basic (5 DTU) | Standard (S0) | Premium / Serverless |
| Cosmos DB | Serverless | Provisioned (400 RU) | Provisioned (autoscale) |
| Storage | Standard_LRS | Standard_GRS | Premium_LRS |
| Static Web Apps | Free | Standard | Standard |
| Key Vault | Standard | Standard | Premium (HSM) |
| Service Bus | Basic | Standard | Premium |
| Redis Cache | Basic C0 | Standard C1 | Premium P1 |
| Functions | Consumption | Flex Consumption | Premium EP1 |
| Log Analytics | PerGB2018 | PerGB2018 | PerGB2018 (dedicated cluster) |
> β οΈ **Log Analytics `Free` SKU is deprecated.** ARM rejects it with some API versions and retention settings. Always use `PerGB2018` β first 5 GB/month is free anyway.
## Modifier Rules
- `intent.scale = "Large"` (100K+ users) β bump minimum one tier above cost-optimized
- `intent.budget = "performance"` + `intent.scale = "Small"` β don't over-provision; use balanced tier
- Policy denies a SKU β fall back to next available tier, add to `assumptions[]`
- Cosmos DB Serverless: max 1K RU/s burst, no geo-replication β only for intermittent/dev workloads. Sustained read-heavy β Provisioned.
## Default Behavior
- Single-component app + cost-optimized β cheapest viable SKU (App Service F1, Static Web Apps Free)
- Simple web app with no DB β $0β$15/month
- Always output exact SKU codes: "App Service F1 (Free)" not "Free tier"
## Auto-Cheapest for Fast-Track
β **If `prereq-output.json.fastTrackEligible == true` AND `context.json.intent.budget` is unset, treat as `cost-optimized`.** Fast-track means single-component + no DB + no auth + no Dockerfile β by definition this app fits the cheapest tier. Pick App Service F1 (Free) for dynamic apps that need a runtime (Node.js/Python starter templates), or Static Web Apps Free for pure static HTML/JS/CSS with no server-side runtime. The user still sees the SKU + monthly cost in the scaffold approval gate β they can override via "Edit plan" if they want to upgrade. Do NOT ask the user about budget unless they mention cost first (per [intent-gathering.md](../../references/intent-gathering.md)).
β **Fast-track free promise can degrade.** If the cheapest free tier is unavailable during deploy prep (no F1 quota AND Static Web Apps Free cap reached β see [sku-quota-validation.md](sku-quota-validation.md) Β§ After Checking), degrade to the **cheapest AVAILABLE** tier β do NOT name or assume a specific paid SKU; let the live quota results decide which is cheapest-available. Record an `assumptions[]` note that states clearly WHY fast-track could not stay free, so the approval gate presents the plan with the resulting cost instead of silently upgrading.
sku-quota-validation.md 8.9 KB
# SKU Quota Validation Procedure
Pre-deploy quota and offer restriction checks. Read during prepare Step 5 before deploying.
For SKU selection (budget tiers, modifier rules, defaults), see [sku-matrix.md](sku-matrix.md).
## Quota Validation Procedure
> β **Use `az rest`, NOT `az quota list`.** The `az quota list` CLI extension triggers a full extension metadata scan on startup. If ANY installed extension has a permission error (common: `azure-devops` WinError 5 on Windows), the entire command fails. `az rest` is a built-in command that bypasses extension loading entirely and hits the same REST API. Quota increases are free β you only pay for resources actually used.
> β **`what-if` does NOT catch App Service quota errors.** `az deployment sub what-if` returns `Succeeded` even when the target SKU has limit=0. The quota rejection only surfaces at actual `az deployment sub create` time. This means the prepare-phase quota check is the ONLY pre-deploy safety net β do not skip or shortcut it.
> β **Do NOT use `az vm list-usage`, `az appservice list-locations`, or `mcp_azure_mcp_quota`** for quota checks. They return misleading data β see [Anti-Patterns](#anti-patterns) below.
### Region Selection
Build the scan list dynamically β do NOT hardcode a fixed set of regions:
1. **User's preferred region** β read `context.json.azure.region` or `context.json.overrides[]` for a region preference. If stated, scan that region first.
2. **Nearest alternates** β add 3β4 regions geographically close to the user's preferred region from the global pool below.
3. **If no user preference** β default to a well-supported region (e.g., `eastus2`) and scan 4 alternates from the user's likely geography (infer from subscription tenant location or ask).
**Global region pool:** `eastus2`, `eastus`, `westus2`, `centralus`, `westeurope`, `northeurope`, `australiaeast`, `japaneast`, `southeastasia`, `brazilsouth`
> **Agent: adapt shell syntax to detected environment.** PowerShell shown below; use equivalent syntax on bash/zsh (e.g., `for region in eastus eastus2 ...; do ... done`).
> β **PowerShell `?` in URLs:** When building `az rest` URLs with variable interpolation, PowerShell may strip `?` from `?api-version=`. Always use the URL **inline in double quotes** (as shown below), NOT via a `$url` variable. If you must use a variable, wrap the `?` with a backtick: `` `?api-version= ``.
### Per-Provider Scripts
Use `az rest` for all quota checks. Query BOTH limit AND usage (limit alone is insufficient).
**App Service:** Query quota + usages endpoints per region:
```powershell
$sub = '{subscriptionId}'; $sku = '{sku}'
@('{userRegion}','{alt1}','{alt2}','{alt3}') | ForEach-Object {
$limit = az rest --method get --url "https://management.azure.com/subscriptions/$sub/providers/Microsoft.Web/locations/$_/providers/Microsoft.Quota/quotas/$sku?api-version=2023-02-01" --query "properties.limit.value" -o tsv 2>$null
$used = az rest --method get --url "https://management.azure.com/subscriptions/$sub/providers/Microsoft.Web/locations/$_/providers/Microsoft.Quota/usages/$sku?api-version=2023-02-01" --query "properties.usages.value" -o tsv 2>$null
# limit=0 with used=-1 is the API's "Free tier not offered here" sentinel β treat limit<=0 as BLOCKED and clamp negative usage so 0-(-1) does NOT become a false-positive 1.
$ln = if ($limit) { [int]$limit } else { $null }; $un = if ($used) { [int]$used } else { 0 }
$avail = if ($null -eq $ln) { 'unknown' } elseif ($ln -le 0) { 0 } else { $ln - [math]::Max(0, $un) }
Write-Host "$_ : $sku limit=$limit available=$avail"
}
```
**Container Apps:** `/usages` gives usage+limit in one call:
```powershell
az rest --method get --url "https://management.azure.com/subscriptions/$sub/providers/Microsoft.App/locations/{region}/usages?api-version=2024-03-01" --query "value[?name.value=='ManagedEnvironmentCount'].{used:currentValue, limit:limit}" -o json
```
**Static Web Apps:** No `Microsoft.Quota` provider β Free plan caps at ~10 apps/subscription (per docs; may vary, treat as guideline). Count existing Free apps:
```powershell
az staticwebapp list --query "length([?sku.name=='Free'])" -o tsv
```
At/near cap β treat SWA Free as UNAVAILABLE (no self-service increase β raises need a support request). Fall back per [After Checking](#after-checking).
**Storage** β default limit 250 accounts/region. Rarely exhausted β skip programmatic check unless the plan requires multiple storage accounts.
**Key Vault** β no quota API exists (returns `NotFound`). Default limit ~1000 vaults/subscription. Skip programmatic check.
### Interpret Results
- `available > 0` β AVAILABLE. `available = 0` / `limit <= 0` β BLOCKED (a `limit=0`, `used=-1` response is the API sentinel for "Free tier not offered in this region" β the script clamps it so it does not read as available). 404/empty β fallback candidate.
- `az rest` fails β `quotaValidation: { verified: false, method: "unverifiable" }`.
### After Checking
1. Only offer regions with **confirmed** capacity β "try anyway" on zero/unconfirmed quota is a known deploy failure.
1b. **Free tier missing in requested region but present elsewhere** β present BOTH, ranked by cost: (a) free tier in nearest confirmed region ($0, recommended), (b) cheapest tier IN the requested region (show monthly cost + `assumptions[]` note). User picks β never silently relocate (region may be a data-residency/latency requirement).
2. **Free SKU zero in ALL regions** β step down the fallback ladder to the **cheapest available** option (don't jump to a named tier β let live quota decide):
- Static-capable app β SWA Free, but only if its cap isn't reached (see **Static Web Apps** above).
- No free option left β cheapest available paid tier the app supports. This breaks the "free" promise β add an `assumptions[]` note stating why (e.g., "No F1 quota in {checkedRegions} and SWA Free cap reached").
- All tiers exhausted β **HALT**: specify region, switch compute type, request increase at portal, or cancel.
3. Write `prepare-plan.json.quotaValidation`: `{ verified: true, method: "cli", verifiedRegion, verifiedSku, checkedRegions[], failedResources[] }`.
### Offer Restriction Check (Database Services)
> β `what-if`/`validate` do NOT catch `LocationIsOfferRestricted`. Use capabilities API.
| Provider | API Version |
|----------|-------------|
| PostgreSQL | `2022-12-01` |
| MySQL | `2023-12-30` |
```powershell
$sub = '{subscriptionId}'; $provider = 'Microsoft.DBforPostgreSQL'; $apiVer = '2022-12-01'
@('{userRegion}','{alt1}','{alt2}','{alt3}') | ForEach-Object {
$result = az rest --method get --url "https://management.azure.com/subscriptions/$sub/providers/$provider/locations/$_/capabilities?api-version=$apiVer" --query "value[0].supportedFlexibleServerEditions[0].name" -o tsv 2>$null
if ($result) { Write-Host "$_ : $provider AVAILABLE ($result)" } else { Write-Host "$_ : $provider BLOCKED (offer restricted)" }
}
```
> For MySQL: change `$provider = 'Microsoft.DBforMySQL'` and `$apiVer = '2023-12-30'`.
β JMESPath MUST start with `value[0].`. URL MUST include `/locations/{region}/`. Empty/null response = BLOCKED. Write results to `quotaValidation.offerRestrictions[]`.
> β **Select the engine version deterministically from the capabilities payload** β match the app's detected version, upgrading only to the nearest compatible release. The payload lists supported versions at `value[0].supportedFlexibleServerEditions[0].supportedServerVersions[].name` (e.g. MySQL: `5.7`, `8.0.21`, `8.4`, `9.5`). Using the **detected DB version passed by the caller** (from `context.json.detectedServices[]`):
> 1. If the **exact detected version** (or its exact patch) is in the supported list β use it.
> 2. Else use the **lowest supported version whose major β₯ the detected major** (detected `5.7`, supported `[5.7, 8.0.21, 8.4, 9.5]` β `8.0.21`). Picking the lowest compatible major β not the newest β avoids the 60+ minute provisioning hangs seen on brand-new majors (e.g. `9.x`) and keeps compatibility with the app's driver/ORM.
> Return it in the quota output's per-service `version` field; the orchestrator copies it to `prepare-plan.json.services[].version` at plan-write (exact patch required β see [prepare-schemas.ts](prepare-schemas.ts) `version`). Record the bump in `assumptions[]` if the detected version was upgraded.
### Anti-Patterns
β `az quota list` (extension failures), `az vm list-usage` (wrong layer), `az appservice list-locations` (ignores quota), `mcp_azure_mcp_quota` (misleading), `what-if`/`validate` (false positives).
## Deploy Gate Re-Validation
If `quotaValidation.verified == false` at deploy gate: re-run Per-Provider Scripts above. Pass β update quotaValidation. Fail β present alternatives. `az rest` fails β warn and proceed.
## Sub-Agent Delegation
When delegating from prepare Step 5, provide: `subscriptionId`, SKU list from `prepare-plan.json.services[].sku`, preferred region + fallbacks, list of managed database services, and this file's content. See [subagent-quota.md](subagent-quota.md) for the template.
subagent-pricing.md 2.4 KB
# Subagent Template β Cost Estimation (Step 6)
Estimate monthly costs for planned Azure services using Azure Retail Prices API.
## References to Read Internally
Read BOTH before making any pricing calls:
- [pricing-guide.md](pricing-guide.md) β methodology, free-tier shortcut, API patterns, troubleshooting
- [pricing-guide-services.md](pricing-guide-services.md) β per-service filter strings, meter names, formulas
## Input (provided by caller)
| Field | Required |
|-------|----------|
| `services[]` with service type and selected SKU per service | YES |
| `region` β deployment region | YES |
| Budget tier (free / balanced / performance) | YES |
## Output
Return JSON (β€500 tokens):
```json
{
"costEstimate": {
"monthlyTotal": 42.50,
"currency": "USD",
"breakdown": [
{ "service": "App Service", "sku": "B1", "monthlyUsd": 13.14, "formula": "retailPrice Γ 730" },
{ "service": "PostgreSQL Flexible Server", "sku": "Standard_B1ms", "monthlyUsd": 24.82, "formula": "compute + storage" }
],
"freeGrants": [
{ "service": "Container Apps", "grant": "180K vCPU-sec + 360K GiB-sec", "monthlySavings": 0 }
],
"assumptions": [],
"disclaimer": "Estimate based on Azure Retail Prices API. Verify at azure.com/pricing."
}
}
```
## Rules
- β **Do NOT invoke ANY skills** β no `{"skill": "azure-validate"}`, `{"skill": "azure-prepare"}`, or any other skill call. You are a pricing subagent only. Use direct HTTP to `https://prices.azure.com/api/retail/prices` for price queries (MCP pricing was already attempted inline by the caller).
- β Check free-tier shortcut FIRST β if ALL services use free SKUs, return $0 with disclaimer
- β `armSkuName` is case-sensitive β use `B1` not `b1`
- β For services with empty `armSkuName` (Container Apps, Functions Consumption, ACR, Storage, Cosmos, Key Vault): use `filter`/`meterName` matching β a `sku` filter returns `[]`
- β **Monthly multiplier = each meter's `unitOfMeasure`** (`1 Hour`βΓ730, `1/Day`βΓ30 [ACR/registry, SQL DTU], `1 GB/Month`βΓGB, `1 Second`βusage) β NEVER blanket Γ730; applying Γ730 to a `1/Day` meter overstates ~24Γ
- β Use direct HTTP to `https://prices.azure.com/api/retail/prices` for all price lookups. Do NOT use `mcp_azure_mcp_pricing` β it was already tried inline and failed.
- β Never hardcode dollar amounts β always query live prices
- β If pricing API returns 400: verify `--sku` included. Free tiers: skip API
## Token Budget
β€500 tokens for cost estimate report.
subagent-quota.md 3.4 KB
# Subagent Template β Quota Validation (Step 5)
Validate SKU quota and offer restrictions across candidate regions before presenting region choices.
## Critical Rules
- β **Do NOT invoke ANY skills** β no `{"skill": "azure-validate"}`, `{"skill": "azure-prepare"}`, or any other skill call. You are a quota-check subagent only.
- **Read [`sku-quota-validation.md`](sku-quota-validation.md) before executing ANY quota or offer restriction check.** It contains the per-provider API patterns, anti-patterns to avoid, offer restriction checks for database services, region selection logic, and output schema. All procedures live there β follow them exactly.
## Input (provided by caller)
| Field | Required |
|-------|---------|
| `subscriptionId` | YES |
| SKU list from Step 4 (service type + SKU per service) | YES |
| Restricted-offer services (PostgreSQL, MySQL) | If present in plan |
| **Detected DB version per DB service** (from `context.json.detectedServices[]`, e.g. MySQL `5.7`) | β REQUIRED if a DB is in the plan β the version-selection algorithm in [`sku-quota-validation.md`](sku-quota-validation.md) needs it. |
## Output
Return JSON (β€500 tokens):
```jsonc
{
"quotaResults": [
{
"region": "{checked region}",
"services": [
{ "service": "{provider}", "sku": "{sku}", "limit": "{from API}", "used": "{from API}", "available": "{limit - used > 0}" },
// if DB service: add "offerRestricted": "{from capabilities API}", "version": "{selected per sku-quota-validation.md β exact patch, never major-only '8.0'}"
],
"allAvailable": "{true only if ALL services in this region have available=true}"
}
// one entry per checked region
],
"recommendedRegion": "{first region where allAvailable=true}",
"checkedRegions": ["{all regions checked}"],
"offerRestrictions": [
// one entry per DB service+region checked β derive from capabilities API response
{ "provider": "{namespace}", "region": "{region}", "restricted": "{true if blocked}", "reason": "{from API or null}" }
],
"offerRestrictionsVerified": "{true only if every DB service from input has β₯1 entry in offerRestrictions[]}"
}
```
β **Caller:** copy each DB service's returned `version` into `prepare-plan.json.services[].version` at plan-write β scaffold needs the exact patch (ARM rejects major-only `'8.0'`).
## Workflow
1. Read [`sku-quota-validation.md`](sku-quota-validation.md)
2. Run the per-provider quota checks for every SKU across all candidate regions
3. If restricted-offer services are in the input, run the offer restriction check for each database service in each candidate region per `sku-quota-validation.md` Β§ Offer Restriction Check
4. Return results per the Output schema above (β€500 tokens)
## Anti-Patterns (from sku-quota-validation.md β repeated here as guardrails)
- β `az vm list-usage` β wrong provider, misleading data
- β `az appservice list-locations` β lists locations, NOT quota
- β `az appservice list-usages` β wrong scope
- β `mcp_azure_mcp_quota` β unreliable for App Service
- β `az quota list` β extension loading fails on Windows
- β Use `az rest` for ALL quota checks
## Rules
- β Free β unlimited β F1, Consumption, Serverless all have per-subscription, per-region quotas
- β Check BOTH limit AND current usage β `limit=1, usage=1` means FULL
- β For PostgreSQL/MySQL: run offer restriction check per `sku-quota-validation.md` Β§ Offer Restriction Check
## Token Budget
β€500 tokens for quota results report.
validation-rubric.md 2.3 KB
# Validation Rubric
Run all 4 dimensions before writing `prepare-plan.json`. All must pass β a failure in any dimension triggers the [Error Handling](../SKILL.md#error-handling) procedures.
## Dimensions
| Dimension | Pass Criteria |
|-----------|---------------|
| **Goal Alignment** | Every `context.json.intent` field reflected in service/SKU choice. No orphaned services (every service maps to a component or supporting role). `overrides[]` honored. |
| **WAF Alignment** | **Cost:** SKU matches budget; alternatives document cost tradeoffs. **Reliability:** Production plans include zone-redundant SKUs, GRS storage. **Security:** Managed identity + Key Vault; private endpoints where budget allows. **Ops:** Log Analytics + App Insights included. **Performance:** SKU right-sized to scale β no over/under-provisioning. See [Azure WAF Service Guides](https://learn.microsoft.com/en-us/azure/well-architected/service-guides/) for per-service alignment. |
| **Dependency Completeness** | Every service has its dependencies present (Container Apps β Log Analytics; SQL β Key Vault for connection strings; App Service β App Insights for monitoring; all services β Managed Identity for auth). Cross-service references consistent (App Insights β Log Analytics workspace). |
| **Deployment Viability** | SKUs exist in target region (validated by quota/region check). No policy-blocked resources. Resource names conform to Azure naming rules. Quota sufficient or flagged with remediation. No conflicting configs (free-tier SKU with paid-only features). |
## Applying
- **During plan creation (steps 3β7):** Use Goal Alignment and WAF Alignment as selection criteria. Use Dependency Completeness as cross-check after mapping.
- **Before writing (step 10):** Run all 4 as validation pass. Deployment Viability catches issues that surface after quota/naming.
- **On failure:** Fix inline via the [Error Handling](../SKILL.md#error-handling) procedures. Document tradeoffs (e.g., WAF Reliability vs cost-optimized budget) in `assumptions[]`.
## References
- [Azure Well-Architected Framework](https://learn.microsoft.com/en-us/azure/well-architected/) β pillar definitions and tradeoff guidance
- [WAF Service Guides](https://learn.microsoft.com/en-us/azure/well-architected/service-guides/) β per-service WAF alignment checklists
approval-gates.md 7.3 KB
# Approval Gates β Steps 6 & 8
> **Gate summary:** AppOnboard has **2 approval gates**: (1) **Scaffold Gate** (orchestrator Step 6) β approve architecture plan before generating IaC, (2) **Deploy Gate** (orchestrator Step 8 / deploy/SKILL.md Step 4) β approve cost + resource summary before `az deployment`. Both are mandatory and SEPARATE β scaffold approval does NOT imply deploy approval.
## Scaffold Approval Gate (Step 6)
Display the architecture plan for user approval BEFORE generating any files:
> β **Resource group edit is MANDATORY in the gate display.** Show this exact block:
> ```
> π’ **Subscription:** {subscriptionName} (`{subscriptionId}`)
> π¦ **Resource Group:** {rg-name} ({region})
> Want a different name or region? Say "Edit plan".
> ```
> β **Gate MUST show Subscription (name + ID), Resource Group, and Region** as standalone lines above the service table β see [pipeline-rules.md Β§ Approval gates](pipeline-rules.md). Do NOT omit or bury in a table.
Also display: services + SKUs + region + resource names + monthly cost estimate + files to generate. **Check `context.json.overrides[]` for `iacFormat`** β if Terraform, display "Terraform templates (`infra/*.tf`)"; if Bicep (default), display "Bicep templates (`infra/main.bicep`)". Show resource names so the user sees what will be created.
> β **Surface plan assumptions.** If `prepare-plan.json.assumptions[]` is present (e.g., free-tier degradation to a paid SKU), display each note prefixed with β οΈ ABOVE the approval prompt β the user MUST see WHY the cost or SKU differs from the fast-track default. Do not bury or omit them.
Verify file list against target SKU β F1/D1: no Dockerfile (built-in runtime). β **Exclude `azure.yaml` from file list** (see pipeline-rules.md).
> β **Container Apps code deploy preview (when plan includes Container Apps).** After the service table, preview the deploy path: build via ACR, replace placeholder images, redeploy. If `buildRequirements.hasBuildKitSyntax == true`, note ACR-compatible versions will be created.
> β **Data-loss warnings at the gate.** If `prereq-output.json.warnings[]` contains any data-loss findings (SQLite on App Service, in-memory sessions, local file storage), surface them prominently with β formatting ABOVE the approval prompt. The user must see these before approving β do not bury them in a table or omit them.
> β **Database network access (when plan includes PostgreSQL/MySQL).** Show a **Database access** line above the approval prompt: the default `AllowAllAzureServicesAndResourcesWithinAzureIps` (`0.0.0.0`) lets the app connect but opens the server to **all** Azure services. β **Render the exposure as its own bold sentence so a fast "Yes" is still an informed "Yes":** **"Proceeding opens the database to all Azure services β not just this app."** Offer the alternative: *"For private networking (VNet integration + private endpoint), say **'Private access'** β AppOnboard does not build private networking itself, so it will hand off to `azure-enterprise-infra-planner`, which then owns the secure networking design **and** its deployment. AppOnboard stops here β it does not resume afterward."* If the user proceeds (Yes), that counts as consent to the `0.0.0.0` rule.
> β **Private networking redirect.** If the user chooses **Private access** (here or during Edit plan): set `context.json.routeToSkill: "azure-enterprise-infra-planner"` and `routeReason: "private-networking-requested"`, then **HALT** β do NOT generate IaC. Tell the user: *"AppOnboard can't generate private networking (VNet + private endpoint). Handing off to azure-enterprise-infra-planner, which will design the secure topology and deploy it from here β it takes over the rest of the onboarding. Your AppOnboard session is saved for reference."* Then invoke `{"skill": "azure-enterprise-infra-planner"}`. This mirrors the prereq `routeToSkill` halt (SKILL.md Step 3) but fires at the Scaffold Gate.
> β **Pick the prompt variant FIRST (based on whether the plan has a database), then use it verbatim β do NOT paraphrase or reword:**
> - **Plan includes PostgreSQL/MySQL** β **"β
Ready to proceed with scaffolding? (Yes / Edit plan / Private access / Cancel)"** β the `ask_user` choices MUST be exactly: `Yes`, `Edit plan`, `Private access`, `Cancel`.
> - **No database in plan** β **"β
Ready to proceed with scaffolding? (Yes / Edit plan / Cancel)"** β the `ask_user` choices MUST be exactly: `Yes`, `Edit plan`, `Cancel`.
>
> The `ask_user` choices MUST match the prompt text exactly β never name an option in one and omit it from the other (e.g. `Private access` must be a selectable choice, not something the user has to type).
> β **RESPONSE BOUNDARY β MANDATORY.** The approval gate MUST be the LAST content in your response. Do NOT generate any files, write any IaC, create any Dockerfiles, or execute any commands in the same response as the gate. Your next action MUST be reading the user's reply. If the user has not yet responded, WAIT β do not proceed.
Three options:
- **Yes** β proceed to Step 7 (scaffold only β NOT deploy)
- **Edit plan** β ask what to change β write to `context.json.overrides[]` β re-run Step 5 β show updated gate
- **Cancel** β preserve session artifacts for later resumption, stop
## Deploy Approval Gate (Step 8)
This is a SEPARATE gate from Step 6.
> β **Context refresh:** Before presenting this gate, re-read this SKILL.md Steps 8-9 if you have not read them in the last 5 turns. Scaffold reference loading (Steps 6-7) consumes significant context β deploy rules may have been evicted.
Display:
- Validation status from `scaffold-manifest.json.validationResult`
- Self-review summary (count of VERIFIED/PLAUSIBLE/FLAGGED findings)
- Resource group name + region
- Services + SKUs + estimated cost
- End with **"π Ready to deploy? (Yes / Run manually / Edit plan / Cancel)"**
> β **After deploy approval:** Your NEXT action MUST be: read `deploy/SKILL.md`, then read `.copilot-azure/sessions/{id}/deploy-checklist.md`. Do NOT call the `azure-deploy` skill β AppOnboard uses its own embedded deploy sub-skill.
If "Run manually" is selected β point to [deploy-checklist-template.md Β§ Deployment Summary](../deploy/references/deploy-checklist-template.md) for manual execution steps.
If validation failed or any FLAGGED findings exist at L1 (Security) or L3 (Hallucination), block **Yes** until resolved.
Only after user approves: proceed to deploy sub-skill (Step 9). `context.json` already has `currentPhase: "deploy"` from the post-scaffold checkpoint (main SKILL.md Step 7).
> β **Before entering deploy:** β Read [`deploy/SKILL.md`](../deploy/SKILL.md) before any deployment action. After mid-session compaction, re-read `deploy/SKILL.md` Steps 4-8. You MUST write `deploy-result.json`.
> β Deploy via `az deployment sub create` (see [pipeline-rules.md](pipeline-rules.md)). AppOnboard-generated `azure.yaml` found β never delete/overwrite; move to `.copilot-azure/sessions/<id>/replaced-files/` (mirror path).
> β **Container Apps code deploy is NOT optional.** After IaC placeholder deploys, complete: `az acr build` β update image params β redeploy β health check. Do NOT present manual CLI "Next Steps" for core deploy tasks. If `hasBuildKitSyntax`, create `Dockerfile.azure` first.
> β **Phase exit:** `deploy-result.json` MUST be written before proceeding to Step 9. See deploy/SKILL.md for the full exit gate checklist.
azd-template-routing.md 4.7 KB
# azd Template Routing
When prereq detects an existing azd template, AppOnboard routes to `azure-prepare` instead of continuing the greenfield pipeline. AppOnboard is a greenfield deployment skill β repos with existing Azure IaC belong to the prepare β validate β deploy pipeline.
## Detection
Before the scope triage question (Step 2), do a quick file-system check (workspace root + `infra/` only β never scan `.copilot-azure/`). Route if ALL of these are true:
| Condition | Where to check |
|-----------|----------------|
| `azure.yaml` exists in workspace root | File system scan |
| `*.bicep` or `*.tf` files exist in `infra/` | File system scan |
| `azure.yaml` has `services:` with at least 1 entry | Read `azure.yaml` in workspace root |
If only `azure.yaml` is present without IaC files (partial azd setup), continue AppOnboard pipeline β the repo needs IaC generated.
## Gate β presented as the scope triage question
When an azd template is detected, the scope triage question is replaced with an azd-aware version (see [intent-gathering.md Β§ Scope triage](intent-gathering.md)). Present:
```
π¦ **Existing Azure deployment setup detected**
Your repo already has:
- `azure.yaml` β Azure Developer CLI configuration
- `infra/` β {Bicep|Terraform} infrastructure templates
{list any other detected infra: Dockerfiles, CI/CD workflows}
This is a complete azd template β it already defines how to build and deploy your app.
**How would you like to proceed?**
1. **Deploy with existing setup** β I'll hand off to `azure-prepare`, which works with azd templates natively. It will analyze your IaC, plan the deployment, and walk you through `azd up`.
2. **Start fresh** β Ignore the existing IaC and build new infrastructure from scratch using AppOnboard's greenfield pipeline. Your existing files won't be modified.
3. **Just scan for readiness** β Keep the prereq results (your app is {ready|needs fixes}) and stop here.
```
## Routing protocol
**Option 1 β Deploy with existing setup:**
1. Write routing state to `context.json`:
```json
{
"routeToSkill": "azure-prepare",
"routeReason": "existing-azd-template",
"completedPhases": [],
"currentPhase": null,
"statusSummary": "Routed to azure-prepare β existing azd template detected, prereq scan skipped (not needed for existing IaC)"
}
```
2. Tell the user:
```
β
Your app is healthy β prereq scan found no blockers.
Since your repo has a complete azd setup, I'm handing off to **azure-prepare** β it's purpose-built
for repos with existing IaC and works natively with `azd up`.
```
3. **Invoke azure-prepare directly:** Call `{"skill": "azure-prepare"}` to load the skill, then follow its workflow using the user's original prompt from `context.json.intent.userPrompt`. This is the same pattern as prereq invocation in Step 3 β AppOnboard loads the skill and the agent follows its instructions. Do NOT ask the user to type a command.
4. **STOP the AppOnboard pipeline.** Do NOT continue to Step 5 (plan architecture). Do NOT generate IaC. Do NOT run `azd up` yourself. azure-prepare owns the rest of the conversation.
**Option 2 β Start fresh:**
1. Write override to `context.json.overrides[]`: `{ "key": "ignoreExistingInfra", "value": "true", "reason": "User chose greenfield over existing azd template" }`
2. If `infra/` directory exists, rename it to `infra.bak/` (single folder rename). This preserves the user's existing IaC as a backup before scaffold writes new files.
3. Continue AppOnboard pipeline from Step 5 (plan architecture).
4. Scaffold Step 3 is skipped (override exists) β the backup was already done here.
**Option 3 β Just scan:**
1. Update `context.json.statusSummary` to reflect the scan-only outcome.
2. Present prereq results summary. STOP.
## Edge cases
| Scenario | Handling |
|----------|----------|
| `azure.yaml` exists but `infra/` is empty | NOT an azd template β continue AppOnboard (repo needs IaC) |
| `azure.yaml` exists with `infra.provider: terraform` | Route same as Bicep β azure-prepare handles both |
| User chose "Start fresh" then hits scaffold guard | Scaffold guard bypassed via `ignoreExistingInfra` override |
| Prereq found blockers AND repo has azure.yaml | Present blockers first (prereq triage), then present azd gate. Blockers take priority. |
| `azure.yaml`/`infra/` looks like it might be an AppOnboard leftover | It's ours only if **any** `.copilot-azure/sessions/*/scaffold-manifest.json` `files[]` lists it (checking every session, not just the active one, catches leftovers from a prior abandoned run); otherwise treat as the user's β route β STOP. β Never decide by git commit status; never delete/overwrite β move to `.copilot-azure/sessions/<id>/replaced-files/` (mirror path). |
handoff-protocol.md 7.5 KB
# Handoff Protocol β Step 9
Offer next steps: CI/CD setup, monitoring, domain config, **ποΈ resource cleanup**, skill-based suggestions. Session artifacts remain for deferred pickup.
> β **Handoff MUST include ALL FOUR sections: (1) Deployment Identity, (2) Cleanup Commands, (3) Redeploy Command, (4) Post-Deploy Recommendations.** Missing any section = incomplete handoff. Do NOT skip cleanup even if deployment failed. Do NOT skip identity even if no resources were created. Do NOT skip recommendations even if the list is empty (print "No post-deploy recommendations.").
## Deployment Identity
> β **Start handoff with deployment identity.** First lines of the handoff response MUST be:
> ```
> π’ Subscription: {context.json.azure.subscriptionName} ({context.json.azure.subscriptionId})
> π Resource Group: {context.json.azure.resourceGroup}
> π Region: {context.json.azure.region}
> π Portal: https://portal.azure.com/#@/resource/subscriptions/{subId}/resourceGroups/{rgName}/overview
> ```
> Source: `context.json.azure`. This is the user's quickest path to finding their resources after the session ends.
See [deploy-checklist-template.md Β§ Deployment Summary](../deploy/references/deploy-checklist-template.md) for the full deployment summary format.
## Artifact Self-Check
> β **Artifact self-check β MANDATORY before handoff.** Verify these exist before presenting cleanup or next steps:
> 1. `deploy-result.json` in session folder β if missing, read [`deploy-schemas.ts`](../deploy/references/deploy-schemas.ts) and write it NOW with status, endpoints, health, `orphanedResourceGroups[]`
> 2. Portal deployment link printed in chat β if missing, generate from `$resId` pattern (see deploy/SKILL.md Step 6) and print now
> 3. `deployment-summary.md` in session folder β if missing, `create` it NOW with the same content you are about to present in chat (status, subscription, RG, region, services table, endpoints, cleanup commands). One `create` call β do NOT skip.
## Post-Deploy Recommendations
> β **`postDeployRecommendations[]` MUST be surfaced β not silently dropped.** Read `prepare-plan.json.postDeployRecommendations[]` (already merged with prereq findings by the prepare phase). Present EACH entry as a numbered recommendation with this format:
> ```
> π Post-deploy recommendations:
> 1. **{title}** ({effort} effort) β {reason}. Services: {services[]}
> 2. ...
> ```
> This section MUST appear BEFORE the skill-based suggestions and AFTER the cleanup commands. If `postDeployRecommendations[]` is empty, print "No post-deploy recommendations." Do NOT skip this section β findings buried in JSON artifacts are easily dropped from the handoff if not explicitly surfaced here.
## Cleanup Commands
> β **Cleanup commands are MANDATORY β always print BOTH.** Every handoff must include these two cleanup blocks, regardless of whether healing occurred or orphans exist.
**Primary cleanup (always print):**
```
ποΈ Delete this deployment's resources:
β az group delete -n {rg} --yes --no-wait
```
**Tag-based bulk cleanup (always print β catches orphans from healing):**
```
π·οΈ Delete ALL resources from this AppOnboard session:
β az group list --tag app-onboard-session-id={sessionId} --query "[].name" -o tsv | ForEach-Object { az group delete -n $_ --yes --no-wait }
```
This catches orphaned RGs from region fallback or naming conflict healing. For Terraform: `cd infra && terraform destroy`.
**If `deploy-result.json.orphanedResourceGroups[]` is non-empty**, list each explicitly with delete commands. For orphans with a `subscription` field, include `--subscription {subscription}`.
## Redeploy Command
> β **Redeploy command is MANDATORY.** The full AppOnboard pipeline (prereq β prepare β scaffold β deploy) is a one-time setup. After that, the user only needs to rebuild and push code. Without this command, they'd have to reverse-engineer the deploy steps from session artifacts or re-run the entire pipeline. Give them the shortcut.
**Derive from what you ran in deploy Step 6b (code deploy).** Do NOT hardcode per service type β echo back the actual command(s) you executed to deploy code. The command varies by compute type, registry name, Dockerfile path, image tag, app name, etc. β all of which you already know from this session.
```
π Redeploy (after code changes):
β {code deploy command(s) from Step 6b}
```
Include this in the chat handoff AND in `deployment-summary.md`.
**If `deploy-result.json.healingAttempts[]` is non-empty**, surface: "βοΈ Deployment required {N} healing attempts" with per-attempt error/action/outcome and planLevelChange details.
**Cross-session cleanup (optional β show if user asks):**
```
π Find ALL AppOnboard resources across all sessions:
β az group list --tag app-onboard-skill=true -o table
```
## Skill-Based Next Steps
> β **Skill-based next steps are MANDATORY.** Always suggest at minimum `azure-compliance` and `azure-resource-visualizer`. Evaluate every condition below.
> β **Suggest, don't self-execute.** Present these as optional suggestions. The deploy agent must NEVER perform post-deploy infra changes itself β zone redundancy, private endpoints, SKU upgrades, and resource re-creation are imperative mutations the deploy phase must not make. If the user explicitly opts into one, route to that skill as a new, scoped task; otherwise the pipeline ends at handoff.
| Condition | Suggest |
|-----------|--------|
| Always | **`azure-compliance`** β "Run a compliance scan on your deployed resources" |
| Always | **`azure-resource-visualizer`** β "Generate an architecture diagram of your resource group" |
| Reliability/HA gaps (single-zone, no failover, prod workload) or user asks to "harden" / "make production-ready" | **`azure-reliability`** β "Assess & improve reliability: zone redundancy, multi-region failover, health probes" |
| Cost-sensitive workload or user asks about spend | **`azure-cost`** β "Review and optimize your Azure spend" |
| Health check failed / `healthStatus: "degraded"/"unreachable"` | **`azure-diagnostics`** β "Troubleshoot your deployment" |
| Auth/OAuth/MSAL detected in intent or prereq | **`entra-app-registration`** β "Set up app registration for your auth flow" |
| `postDeployRecommendations[]` has upgrade suggestions | **`azure-upgrade`** β "Upgrade your runtime or framework" |
| Storage-heavy patterns | **`azure-storage`** β "Optimize your storage configuration" |
| `postDeployRecommendations[]` mentions RBAC/role | **`azure-rbac`** β "Configure least-privilege role assignments" |
## Completion
> β **End the handoff with an explicit completion line β the LAST thing you emit.** It marks the pipeline done (so you stop working) and tells the user the core task succeeded and anything more is their choice. Frame it as closure WITH an open door β never a hard stop.
>
> β **Use this EXACT sentence verbatim β do NOT paraphrase, reword, or reorder it:** "The onboarding pipeline is finished." Only `{primary endpoint URL}` varies. This exact phrase is a required completion marker.
```
β
**Deployment complete β your app is live at {primary endpoint URL}.** The onboarding pipeline is
finished. The next steps above are optional β tell me which you'd like and I'll route you to the
right skill.
```
> β **The recommendations above are OPTIONAL β do NOT autonomously execute them.** Present them and let the user choose; if they pick one, route to the matching skill (it makes the change properly). Don't turn a general acknowledgment into a batch of unsolicited hardening. Beyond this point the session is the user's β they may continue, invoke another skill, or run their own commands; that's expected and no longer part of this pipeline.
iac-resources.md 2.6 KB
# IaC Resources β Official Documentation & Tools
Look up when stuck after 3 tries, edge cases, or validating generated code against ground truth.
## Bicep
| Resource | URL | Use When |
|----------|-----|----------|
| Bicep Documentation | https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/ | Syntax, file structure, deployment scopes, install |
| Azure Resource Reference | https://learn.microsoft.com/en-us/azure/templates/ | Resource properties, API versions, schema per type |
## Terraform
| Resource | URL | Use When |
|----------|-----|----------|
| Terraform Registry β azurerm | https://registry.terraform.io/providers/hashicorp/azurerm/latest | Resource type properties, argument reference, import blocks |
| Terraform Registry β azapi | https://registry.terraform.io/providers/azure/azapi/latest | Preview resources not yet in azurerm; maps to ARM REST APIs |
## Validation Tools
| Tool | Format | Purpose |
|------|--------|---------|
| `bicep build` | Bicep | Syntax + schema validation |
| `az deployment group create --what-if` | Bicep | ARM-level dry run with change preview |
| `terraform validate` | Terraform | Syntax + schema validation |
| `terraform plan` | Terraform | Provider-level dry run |
## Deploy Troubleshooting
> β **Primary lookup path:** Call `mcp_azure_mcp_documentation` with the error message first. Use the table below as fallback when MCP is unavailable or returns no results.
>
> **On repeat failures (same error 2+ consecutive attempts):** `fetch_webpage` the matching URL below with the error message as query. Apply the documented fix β do not retry the same approach.
| Resource | URL | Use When |
|----------|-----|----------|
| Common ARM Deployment Errors | https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/common-deployment-errors | `InvalidTemplateDeployment`, `SkuNotAvailable`, `QuotaExceeded`, any ARM error code |
| App Service Troubleshooting | https://learn.microsoft.com/en-us/troubleshoot/azure/app-service/ | Startup crashes, Kudu/Oryx build failures, health probe issues |
| App Service Zip Deploy Guide | https://learn.microsoft.com/en-us/azure/app-service/deploy-zip | Zip deploy, SCM_DO_BUILD_DURING_DEPLOYMENT, Kudu publish API |
| Container Apps Troubleshooting | https://learn.microsoft.com/en-us/azure/container-apps/troubleshooting | Revision failures, ingress errors, secret resolution, image pull failures |
| Quota Increase Portal | https://portal.azure.com/#blade/Microsoft_Azure_Capacity/QuotaMenuBlade/myQuotas | Direct link for quota increase requests |
> **Source:** Official Microsoft Learn, HashiCorp Developer, and Azure documentation.
intent-gathering.md 2.2 KB
# Intent Gathering
## Scope Triage (Step 2 β before prereq)
> β **Scope triage β BEFORE prereq.** Check for azd template markers (`azure.yaml` + IaC in `infra/`).
>
> **Skip triage when:**
> - User explicitly asks for cost estimates, service recommendations, or code analysis
> - Empty workspace (prereqβs zero-code-path handles it)
> - Code exists but no infra files (no `.bicep`, `.tf`, `azure.yaml`, or `infra/` dir) β full pipeline is the only sensible path
> - Intentionally vulnerable app signals (π detection) β proceed to prereq directly
>
> ### If azd template detected
>
> β **You MUST read [`azd-template-routing.md`](azd-template-routing.md)** for detection criteria, gate presentation, and routing protocol.
>
> ### If NO azd template BUT infra files present
>
> Ask ONE `ask_user` question:
> 1. **Yes β analyze and deploy end-to-end** (Recommended)
> 2. **Just scaffold Bicep/Terraform**
> 3. **Just deploy it** (I have IaC ready)
> 4. **Other**
>
> Option 1 / vague β full pipeline (Step 3). Any other β invoke `{"skill": "azure-prepare"}`.
## After Prereq Returns (Step 4 β scan-informed intent gathering)
Prereq has written `prereq-output.json` and `context.json.components[]` β this is the authoritative source for all downstream phases (prepare and scaffold consume `context.json`, not `prereq-output.json`).
Confirm the Azure target ("βοΈ **Azure target**: {subscriptionName} ({subscriptionId})"). If the user wants a different subscription, write to `context.json.overrides[]`.
**Present scan results first, then ask only what prereq didn't answer** (β€2 if mostly covered, β€4 if gaps):
| # | Topic | Ask if... |
|---|-------|----------|
| 1 | App purpose | Not obvious from `detectedStack` + `components[]` |
| 3 | Data/storage | Prereq didn't detect DB/compose |
| 4 | Auth approach | No MSAL/passport/auth library detected |
| 5 | Scale | Always β prereq doesn't know traffic expectations |
β Do NOT ask about stack/language (always answered by prereq) or budget (only if user mentioned cost). User corrections β `context.json.overrides[]`. Stop when covered or user says "just go."
**Update intent:** Merge scan results into `context.json.intent`. Set `refinedFromScan: true` and populate `scanDiscoveredFacts[]`.
mcp-tool-reference.md 3.7 KB
# MCP Tool Reference β Shared Index
Shared tools used across multiple AppOnboard phases, plus the cross-cutting PhaseβTool Map. For phase-specific tools with full parameter tables, see the per-phase references linked below.
> **Troubleshooting:** If a tool call fails with unknown parameter or missing command errors, consult the official docs for current parameter names and allowed values: <https://learn.microsoft.com/en-us/azure/developer/azure-mcp-server/tools/>
## Per-Phase Tool References
| Phase | File | Exclusive Tools |
|-------|------|-----------------|
| Prereq | *(shared tools only β see table below)* | *(shared only)* |
| Prepare | [mcp-tools.md](../prepare/references/mcp-tools.md) | pricing, quota, cloudarchitect, WAF, advisor, group_resource_list, policy |
| Scaffold | [mcp-tools.md](../scaffold/references/mcp-tools.md) | bicepschema, all mcp_bicep_*, deploy (iac_rules/pipeline/plan), terraform best practices |
| Deploy | [mcp-tools.md](../deploy/references/mcp-tools.md) | resourcehealth, monitor, appservice, deploy (app_logs/arch_diagram), role |
---
## Global Parameters (all tools)
Every Azure MCP tool accepts these optional parameters in addition to its own:
| Parameter | Description |
|-----------|-------------|
| `subscription` | Azure subscription ID or display name. Defaults to `az account show` default. |
| `tenant` | Entra ID tenant GUID or name. Uses default tenant if omitted. |
| `resource-group` | Resource group name. Required for most resource-specific operations. |
> **Additional global params** (not commonly needed by AppOnboard): `authentication-method` (credential\|key\|connectionString), `max-retries` (default 3), `retry-delay` (default 2s), `retry-delay-maximum` (default 10s), `retry-mode` (fixed\|exponential), `retry-network-timeout` (default 100s). See [official docs](https://learn.microsoft.com/en-us/azure/developer/azure-mcp-server/tools/) for details.
---
## Shared Tools (used by 2+ phases)
### `mcp_azure_mcp_subscription_list`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| *(none)* | `tenant` | β
|
Returns: `subscriptionId`, `displayName`, `state`, `tenantId`, `isDefault`. Use `isDefault: true` as default subscription.
Used by: **prepare** (Step 1), **deploy** (Step 1)
### `mcp_azure_mcp_group_list`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| *(none)* | `subscription`, `tenant` | β
|
Returns: resource group names and IDs as JSON array.
Used by: **prepare** (Step 7), **deploy** (Step 3)
### `mcp_azure_mcp_extension_cli_install`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| `cli-type` (az\|azd\|func) | `tenant` | β
|
Returns: installation instructions for the specified CLI tool.
Used by: **prereq** (Step 2), **deploy** (Step 3)
### `mcp_azure_mcp_get_azure_bestpractices` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `get_azure_bestpractices_get` | `resource` (general\|azurefunctions\|static-web-app\|coding-agent), `action` (all\|code-generation\|deployment) | β | β
|
| `get_azure_bestpractices_ai_app` | *(none)* | β | β
|
**Usage:** `resource` + `action` are both required for `_get`. For `static-web-app` and `coding-agent`, only `action: "all"` is supported.
Used by: **prereq** (Step 3), **scaffold** (Step 5)
---
## Tool Pitfalls
- **`mcp_azure_mcp_subscription_list` is slow at scale:** Returns ALL subscriptions across ALL tenants (238+ in large orgs), causing lengthy picker detours. Use `az account show` for the active subscription (<1 second). Reserve `subscription_list` for prepare Step 1 when the user explicitly wants a different subscription.
pipeline-rules-runtime.md 3.5 KB
# Pipeline Rules β Runtime Reference
Known platform bugs and deploy timing. Read before deploy completion or on scaffold/deploy error.
For core pipeline rules (approval gates, phase lifecycle, session artifacts, security baseline), see [pipeline-rules.md](pipeline-rules.md).
## Known Platform Bugs
| Bug | Symptom | Workaround |
|-----|---------|------------|
| SWA CLI Windows path with spaces | `StaticSitesClient.exe` fails when workspace path contains spaces | Use a short, space-free temp path on FIRST attempt (e.g., `C:\temp\swadeploy` on Windows, `/tmp/swa-deploy` on macOS/Linux) β do NOT wait for retry. Copy app content to the temp path before running `swa deploy`. |
| `az deployment sub validate` HTTP stream | "HTTP response stream consumed" error (Azure CLI bug, also affects `create` in CLI 2.75.0+) | `validate`: use `az deployment sub what-if` instead. `create`: use `az rest --method PUT` on deployment URI as fallback. |
| `az quota list` extension error | `PermissionError: [WinError 5]` blocks extension commands | Use `az rest` instead (see [sku-quota-validation.md](../prepare/references/sku-quota-validation.md)) |
| Managed identity sidecar OOM on free/basic-tier Linux | `503 Service Unavailable` with `Microsoft.Azure.WebSites.DataProtection` or `/msi/token` timeout | Avoid managed identity on free/basic-tier Linux compute (F1, B1) β use connection strings or upgrade to S1+ |
| `Compress-Archive` path flattening | PowerShell's `Compress-Archive -Path $files.FullName` uses absolute paths, flattening directory structure | Use `System.IO.Compression.ZipFile` with relative paths instead |
| AADSTS530084 (Terraform) | Token protection conditional access policy breaks `azurerm` provider auth; regular `az` CLI commands work fine | Re-scaffold as Bicep |
| Secret values with shell special chars | Passwords containing `$`, `` ` ``, `!`, `'`, `"` break when passed as inline CLI args (`--parameters key=val`) | **ALWAYS pass secrets via `main.parameters.json` or `terraform.tfvars`** β never as inline `--parameters` args. For deploy-phase secret seeding (`az keyvault secret set`), use `--file` with a temp file or pipe from stdin to avoid shell interpolation. |
| `az acr task logs` encoding crash | `UnicodeEncodeError` on Windows from Unicode chars in build logs | Use `--no-format` + strip non-ASCII, or REST API `listLogSasUrl` |
| `create` tool nested path failure | `"Parent directory does not exist"` when creating files in `.copilot-azure/sessions/{id}/` | Run `New-Item -ItemType Directory -Path {parent} -Force` before `create`. Platform tool limitation β agent always recovers. |
| Windows PowerShell `az rest` 415 error | `az rest --method put --body '{json}'` returns `415 Unsupported Media Type` on Windows PowerShell | Add `--headers "Content-Type=application/json"` to every `az rest --method put` call |
## Deploy Timing
- **F1 App Service cold-start:** After IaC creation, F1 plans need 30β120 seconds before the Kudu sidecar is ready. Deploying code immediately after `az deployment group create` returns causes 504 Gateway Timeout. Wait for sidecar readiness before code deployment.
- **Identity tag resolution:** Resolve `deployed-by` once at session start via `az ad signed-in-user show`. Without this, resources may receive inconsistent tag values (display name, UPN, or hardcoded strings) across different runs.
## Shell Rules
β Shell variables do NOT persist between tool calls. Generate secrets inline within the command that consumes them β never store in `$env:` for later use. Use synchronous shells for all deploy-phase operations.
pipeline-rules.md 6.2 KB
# Pipeline Rules β Reference
Cross-cutting rules enforced across all workflow steps. Referenced from [SKILL.md](../SKILL.md) `## Pipeline Rules`.
## Approval gates
β **Two separate approval gates are required β never merge them.**
1. **Scaffold gate (Step 6):** "β
Ready to proceed with scaffolding? (Yes / Edit plan / Cancel)" β approves IaC generation only. β When the plan includes PostgreSQL/MySQL, add `Private access` as a selectable choice β see approval-gates.md for the exact variant.
2. **Deploy gate (Step 8):** "π Ready to deploy? (Yes / Run manually / Edit plan / Cancel)" β approves resource provisioning.
The scaffold gate does NOT grant deploy permission. After scaffold completes, you MUST present the deploy gate as a SEPARATE response. Never go from scaffold approval directly to `az group create` or `az deployment`.
β **BOTH gates MUST show Subscription (name + ID), Resource Group, and Region** as standalone lines above the service table β users must see WHERE resources will be created before approving.
β **NEVER create, write, or modify infrastructure files before the user explicitly says "Yes" to the scaffold gate.** No exceptions β not for simple apps, trivial plans, free-tier deployments, or single-component repos.
β **Modifying existing IaC files (not AppOnboard-generated) requires explicit user approval.** Present: "I need to modify {file}: {change description}. Approve? (Yes / Edit / Cancel)". This applies to repos with existing Bicep/Terraform where AppOnboard adjusts SKUs, regions, or settings.
Each gate is the LAST content in its response β do NOT continue past a gate in the same turn.
> β BAD: Writes Bicep without approval Β· scaffolds and deploys in same response Β· skips deploy gate after scaffold approval
> β
GOOD: Shows plan β user says Yes β scaffold β show validation summary β deploy gate β user says Yes β deploy
## Phase lifecycle
Update `context.json` at phase boundaries β combine `completedPhases` update with `currentPhase` for the next phase in a single write. Write the phase artifact before marking complete. The orchestrator SKILL.md specifies exact write points (after prereq, after scaffold, after deploy).
`currentPhase` must NEVER appear in `completedPhases` β if invariant violated, halt and report.
`context.json` is NOT write-once β each phase boundary MUST update it on completion:
- Write `intent` after Step 2
- `components` after Step 3
- `azure.resourceGroup` after Step 7 (also written to `deploy-result.json.resourceGroupName`)
- Push to `completedPhases` at phase boundaries
- Update `statusSummary` at every phase exit β 1-line description. Templates:
- prereq: `"{N} components, stack: {detectedStack}, health: {overallHealth}"`
- prepare: `"{N} services, ~${monthlyUsd}/mo, region: {region}"` (if `quotaValidation.checkedRegions` >1, append fallback reason: `"region: westus2 (eastus quota full)"`)
- scaffold: `"{N} files, self-review: {VERIFIED|FLAGGED count}"`
- deploy: `"{healthStatus}, RG: {resourceGroupName}"`
- cancel: `"Paused at {phase} β {reason}"`
## Session artifacts
**Session file writes: `New-Item -ItemType Directory` for directories, `create` tool for file content.** Create session directory via `New-Item -ItemType Directory -Path ".copilot-azure/sessions/{uuid}" -Force`, then `create` tool for all JSON/md content. Never use `Out-File`, `Set-Content`, or shell commands for file content.
## Phase transition rule
> β **Before executing the FIRST command of any new phase, re-read that phase's sub-SKILL.md.** After prereq β read `prepare/SKILL.md`. After prepare β `scaffold/SKILL.md`. After scaffold gate β `deploy/SKILL.md`. This applies at EVERY transition.
## Post-compaction recovery
> β After ANY compaction, re-read current phase SKILL.md + this file. Check `scaffold-manifest.json` and `completedPhases` exist if mid-scaffold/deploy.
Begin responses with: "Started session at `.copilot-azure/sessions/{uuid}/`" or "Resuming session from [date] β {statusSummary}".
β **Session immutability:** NEVER write to any session folder other than the active session (the one `.copilot-azure/sessions/active-session.json` points to). Old sessions are read-only β no updates, no backfills, no status changes.
**Session TTL:** 7 days. Non-active sessions where `context.json.lastModifiedUtc` is >7 days ago are deleted on next invocation. The active session is never pruned.
## fastTrackEligible
Set by prereq: (1) auto-approves readiness gate, (2) simplifies prepare Step 3 alternatives. Does NOT skip phases, reads, gates, self-review, validation, or preflight.
## Deploy as-is
β Do NOT refactor or upgrade working application code. Deploy what works. Fixing broken code IS allowed (build errors, missing deps) through the approval gate. Upgrade suggestions β `prepare-plan.json.postDeployRecommendations[]`. Infrastructure changes = allowed; code rewrites = forbidden; Azure compatibility changes (TLS, SSL, port) = allowed when detected by prereq AND approved. Never prompt for passwords β auto-generate into Key Vault.
## Known Platform Bugs
See [`pipeline-rules-runtime.md`](pipeline-rules-runtime.md) Β§ Known Platform Bugs for the full bug table and workarounds.
## No top-level skill invocation
β **NEVER call external skills** (`azure-validate`, `azure-deploy`, `azure-prepare`, etc.) during the AppOnboard pipeline. Only `azure-app-onboard-prereq` and `azure-app-onboard` orchestrator are allowed. Use direct CLI commands for validation and deployment.
## Structured sub-agent delegation
β Use ONLY `subagent-*.md` templates β no ad-hoc prompts. Pass template content verbatim. Destructive commands (`az deployment`, `az webapp deploy`, `az acr build`) execute in main thread only.
## Security baseline
See [iac-generation-rules.md](../scaffold/references/iac-generation-rules.md) Β§ Security Patterns and [bicep-patterns-security.md](../scaffold/references/bicep-patterns-security.md). Flag `AllowAzureServices` firewall rule as a security warning.
## azure.yaml prohibition
β **NEVER generate `azure.yaml`. NEVER use `azd up`/`azd provision`/`azd deploy`.** AppOnboard deploys via `az deployment sub create` (Bicep) or `terraform apply` (Terraform). Repos with existing `azure.yaml` β route per [`azd-template-routing.md`](azd-template-routing.md).
session-protocol.md 7.9 KB
# Session Protocol β Step 1
## All Prompts Are Actionable
> β **ALL prompts that activate this skill are actionable β go directly to Step 1.** Do NOT answer the user's question, give an overview of capabilities, or describe what AppOnboard can do before starting the pipeline. "Can Azure figure out my app?" and "Deploy my app" are the same action: Step 1 β Step 2 β scan. The user's phrasing (question vs command) does NOT change the workflow.
## Session Check
Resolve active session via pointer file.
> β **YOU MUST CREATE A SESSION BEFORE DOING ANY WORK β INCLUDING SCANNING**
>
> 1. **STOP** β Do not answer the user's question, scan code, or plan architecture yet
> 2. **CHECK** β Read `.copilot-azure/sessions/active-session.json`.
> - β **First, ensure the repo's `.gitignore` contains `.copilot-azure/`** (append if missing, create the file if absent) β this runs on EVERY path below, BEFORE any branch writes a session file, since session artifacts may hold deploy secrets.
> - **Pointer exists** β β **You MUST read [`session-schemas.ts`](session-schemas.ts)** to get the exact field names and types for `AppOnboardContext`. Do not guess field names. Then read the pointed-to session's `context.json`. Display: "Found session from [lastModifiedUtc] β {statusSummary}." β **You MUST ask the user via `ask_user`: "Resume this session or start fresh?" Do NOT auto-resume.** This gate is mandatory β stale sessions from prior tests cause the agent to skip sub-SKILL.md reads and miss artifact writes.
> - Resume β β **Read the sub-SKILL.md for the NEXT phase** (derive from `completedPhases`). E.g., prereq done β read `prepare/SKILL.md`. Then continue from that phase.
> - β **If `context.json.routeToSkill` is set:** The previous session was halted for migration (e.g., `azure-cloud-migrate`). The code has likely changed since then. **Do NOT skip prereq** β start fresh: clear `routeToSkill`, `routeReason`, remove `"prereq"` from `completedPhases`, and re-run from Step 2. This ensures the migrated codebase gets a clean 3-axis evaluation.
> - β **If `completedPhases` includes `"prereq"` (and no `routeToSkill`):** Prereq already wrote `prereq-output.json` and `context.json.components[]`. Proceed to Step 2 (scope triage) β prereq may have been invoked standalone, so the user still needs to confirm the full pipeline. Skip Step 3 (prereq invocation), then continue to Step 4 (scan-informed intent gathering).
> - Start fresh β generate a new UUID via `[guid]::NewGuid().ToString()`, create a new session folder, update `active-session.json` to point to the new session. Old session folder is never touched again.
> - **Pointer missing but session folders exist** β list folders under `.copilot-azure/sessions/`. If 1 folder: adopt it (read its `context.json`, write `active-session.json` pointing to it, show summary). If 2+: show a numbered list with `statusSummary` + `lastModifiedUtc` from each, ask user to pick one or start fresh. Write pointer for the chosen session.
> - **No sessions at all** β generate a UUID by running `[guid]::NewGuid().ToString()` in the terminal. β **You MUST generate the UUID via a terminal command β do NOT hardcode a placeholder like `a1b2c3d4-e5f6-7890-abcd-ef1234567890`.** Create the session directory: `New-Item -ItemType Directory -Path ".copilot-azure/sessions/{uuid}" -Force`. Then write a **minimal** `context.json` using the `create` tool β only these 3 fields are known immediately: `{ "sessionId": "{uuid}", "createdUtc": "{ISO 8601 now}", "intent": { "userPrompt": "{user's first message verbatim}" } }`. Write `active-session.json` with `activeSessionId: {uuid}` using the `create` tool.
> 3. **PRUNE** β After resolving the active session, check remaining session folders. Delete any where `context.json.lastModifiedUtc` is >7 days ago. **Never delete the active session** (the one `active-session.json` points to).
> 4. **VERIFY** β Confirm `context.json` exists and is valid JSON. If missing or malformed, halt and retry creation β do NOT continue to Step 2 without a verified session.
> 5. **CONFIRM** β Begin your first response with: "Started session at `.copilot-azure/sessions/{uuid}/`" (new) or "Resuming session from [date] β {statusSummary}" (existing)
> 6. **THEN** proceed to Step 2
>
> β **Ordering: session FIRST, scanning SECOND.** If you scan the workspace or read project files before writing `context.json`, you have violated the session-first rule. The session must exist before ANY code analysis.
>
> β **Shell fallback:** If PowerShell/terminal hangs on first attempt (no output after 10s), use the `create` tool directly for session directory and file writes. Do NOT retry shell commands more than once.
>
> β **Path scoping: ALL `create` tool calls for session artifacts MUST target `.copilot-azure/sessions/{active-session-id}/`.** Writing to any other session folder is forbidden.
## CLI Availability
Call `mcp_azure_mcp_extension_cli_install` with `cli-type: "az"` to verify Azure CLI is available. If missing, surface installation instructions before proceeding. Downstream phases (prepare, deploy) require it. Fallback: skip if MCP tool unavailable.
## Azure Login Gate
**Azure login gate (mandatory):** Run `az account show --query "{id:id, name:name, tenantId:tenantId}" -o json` with a **15-second timeout** (PowerShell: `Start-Process` with `-Wait` or inline timeout; if command hangs beyond 15s, treat as failure). Also run `az ad signed-in-user show --query displayName -o tsv` (15-second timeout). After BOTH commands complete, merge ALL azure fields into `context.json.azure` in a **SINGLE update** β `subscriptionId`, `subscriptionName`, `tenantId`, and `userDisplayName`. Do NOT write separate updates for subscription and identity.
> β **If `az account show` fails or hangs:** β **You MUST read [`subscription-resolution.md`](subscription-resolution.md)** and follow its fallback procedure. Do NOT proceed to Step 2 without a resolved subscription. Do NOT leave `context.json.azure` empty and continue. Every downstream phase (prepare, scaffold validation, deploy) requires Azure auth β proceeding without it produces incomplete results.
## User Identity Detection
**User identity detection (for `deployed-by` tag):** Run `az ad signed-in-user show --query displayName -o tsv` (15-second timeout) alongside `az account show`. Fallback if `az ad` fails: use `az account show --query user.name -o tsv` (returns UPN/email). If both fail, leave empty β prepare phase will resolve. This value becomes the `deployed-by` tag on ALL resources β resolving it once here prevents inconsistent tag values across resources. **Merge into the SAME `context.json` update as the azure login gate β do NOT write separately.**
## Subscription Detection Method
> β **`az account show` is the ONLY subscription detection method in Step 1.** Do NOT call `mcp_azure_mcp_subscription_list` here β that tool returns ALL subscriptions across ALL tenants and causes a lengthy picker detour. `az account show` returns the CLI's active subscription in <1 second. MCP subscription list is reserved for prepare Step 1 when the user explicitly wants a different subscription.
## Artifact Locations
| Location | Artifacts |
|----------|-----------|
| `.copilot-azure/sessions/{uuid}/` | `context.json`, `prereq-output.json`, `prepare-plan.json`, `scaffold-manifest.json`, `deploy-result.json` |
| `.copilot-azure/sessions/{uuid}/replaced-files/` | User files displaced by scaffold (existing IaC), stored at their original relative path (**mirror path** = same directory structure as the repo). Never overwritten or deleted β moved here so the original is preserved. |
## Phase-gated Reference Loading
> β **Phase-gated reference loading.** Do NOT pre-read reference files for downstream phases. Read each sub-skill's references only when entering that step. Scaffold references (bicep-patterns, self-review) are irrelevant during deploy; prepare references (service-mapping, pricing-guide) are irrelevant during scaffold. Each sub-skill SKILL.md specifies its own required reads.
session-schemas.ts 5.0 KB
/**
* Context + shared TypeScript interfaces for AppOnboard session artifacts:
* context.json, active-session.json, and shared types used across all phases.
*
* Per-phase schemas (each sub-skill has its own in its references/ folder):
* - azure-app-onboard-prereq/references/prereq-schemas.ts β prereq-output.json
* - prepare/references/prepare-schemas.ts β prepare-plan.json
* - scaffold/references/scaffold-schemas.ts β scaffold-manifest.json
* - deploy/references/deploy-schemas.ts β deploy-result.json
*
* Source of truth for JSON artifacts in `.copilot-azure/sessions/{session-id}/`.
*/
// βββ shared types (used across all phases) βββββββββββββββββββββββββββββββββββ
export interface AppOnboardComponentStack {
language: string;
framework: string;
version: string;
}
export type ReadinessStatus = "ready" | "fixesApplied" | "needsFixes" | "unknown";
export interface AppOnboardComponentReadiness {
status: ReadinessStatus;
fixes: string[];
}
export type VerdictLevel = "PASS" | "WARN" | "FAIL" | "SKIPPED";
export interface AppOnboardComponentVerdicts {
build: VerdictLevel;
completeness: Exclude<VerdictLevel, "SKIPPED">;
deployability: Exclude<VerdictLevel, "SKIPPED">;
}
export interface AppOnboardComponentFinding {
category: "build" | "completeness" | "deployability";
verdict: VerdictLevel;
summary: string;
fix: string | null;
}
export interface AppOnboardComponent {
name: string;
path: string;
stack: AppOnboardComponentStack;
readiness: AppOnboardComponentReadiness;
verdicts?: AppOnboardComponentVerdicts;
findings?: readonly AppOnboardComponentFinding[];
}
export interface AppOnboardAzureTarget {
subscriptionId: string;
/** Display name of the subscription (from `az account show --query name`).
* Shown at both approval gates so the user can verify the target. */
subscriptionName: string;
resourceGroup: string;
region: string;
}
export interface AppOnboardRepoInfo {
remote: string | null;
}
export interface AppOnboardOverride {
key: string;
value: string;
reason: string;
}
export interface AppOnboardAppInfo {
name: string;
}
export interface PostDeployRecommendation {
title: string;
reason: string;
effort: "low" | "medium" | "high";
services?: string[];
}
export interface DetectedService {
type: string;
version?: string;
source: "compose" | "config" | "code";
}
// βββ context.json βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export interface AppOnboardIntent {
userPrompt: string;
description: string;
users?: string;
auth?: string;
scale?: string;
budget?: string;
/** Set to true after prereq scan refines intent */
refinedFromScan?: boolean;
/** Facts discovered by the prereq scan */
scanDiscoveredFacts?: string[];
}
export type AppOnboardPhase = "info" | "prereq" | "prepare" | "scaffold" | "deploy" | "cicd" | "observe";
export interface AppOnboardContext {
sessionId: string;
createdUtc: string;
lastModifiedUtc: string;
currentPhase: AppOnboardPhase | null;
completedPhases: readonly AppOnboardPhase[];
/** Human-readable 1-line summary of where the session stands, updated at each phase exit.
* Displayed in the session picker when the user resumes or switches sessions. */
statusSummary?: string;
intent: AppOnboardIntent;
components: AppOnboardComponent[];
azure: AppOnboardAzureTarget;
repo: AppOnboardRepoInfo;
app?: AppOnboardAppInfo;
/** Infrastructure file types detected in repo: dockerfile, terraform, bicep, azure-yaml, github-actions */
detectedInfra: readonly string[];
/** Cloud provider targeted by detected IaC. Only populated when `.tf` or `.bicep` files found.
* Used by scaffold to distinguish "existing Azure IaC" (halt) from "non-Azure IaC" (generate Azure TF alongside). */
detectedInfraProvider?: {
terraform?: "azure" | "gcp" | "aws" | "multi" | "unknown";
};
/** Service dependencies parsed from docker-compose, config files, or code imports */
detectedServices: readonly DetectedService[];
overrides: AppOnboardOverride[];
/** Set when a phase routes the pipeline to another skill β e.g. "azure-cloud-migrate"
* (non-Azure cloud SDK deps) or "azure-prepare" (existing azd template). Presence halts
* the greenfield pipeline; the resume path in session-protocol.md clears it and re-runs prereq. */
routeToSkill?: string;
/** Human-readable reason paired with `routeToSkill` (e.g. "existing-azd-template"). */
routeReason?: string;
}
// βββ active-session.json βββββββββββββββββββββββββββββββββββββββββββββββββββββ
/** Pointer file at `.copilot-azure/sessions/active-session.json`.
* Avoids scanning all session folders on startup β read this one file
* to find the active session, then read that session's context.json. */
export interface ActiveSessionPointer {
activeSessionId: string;
}
// PrereqOutput, BuildRequirements β see azure-app-onboard-prereq/references/prereq-schemas.ts subscription-resolution.md 2.1 KB
# Subscription Resolution β Defensive Fallback
The `azure-app-onboard` orchestrator resolves the subscription at Step 1 (login hard gate) and writes `subscriptionId`, `subscriptionName`, `tenantId` to `context.json.azure` before any sub-skill runs. In normal operation, `context.json.azure.subscriptionId` is always set by the time prepare runs.
At prepare phase entry, verify `context.json.azure.subscriptionId` is set. If it is (expected path), use it β done.
If `context.json.azure` is somehow empty, resolve now rather than halting the flow:
1. **Check env vars** β if `AZURE_SUBSCRIPTION_ID` is set, use it directly (with `AZURE_TENANT_ID` if set). Write `subscriptionId`, `subscriptionName`, `tenantId` to `context.json.azure`, done.
2. **Run `az account show`** β `az account show --query "{id:id, name:name, tenantId:tenantId}" -o json`. If it succeeds, **auto-select** β write `subscriptionId`, `subscriptionName`, `tenantId` to `context.json.azure`. Do NOT run `az account list` or present a picker.
3. **Fallback: `mcp_azure_mcp_subscription_list` + picker** β only if `az account show` fails. Call `mcp_azure_mcp_subscription_list` to retrieve all subscriptions (returns `subscriptionId`, `displayName`, `isDefault`).
- **1 subscription** β auto-select, no question. Write `subscriptionId`, `subscriptionName`, `tenantId` to `context.json.azure`.
- **2+ subscriptions** β present a picker via `ask_user`: list each subscription as a choice `"{displayName} ({subscriptionId})"` with the default marked. The user selects one. Write `subscriptionId`, `subscriptionName`, `tenantId` to `context.json.azure`.
4. **MCP tool fails** β attempt `az login` (interactive browser login). If that fails (no browser, remote session), fall back to `az login --use-device-code`. On success, retry from step 2. **Cap login at 3 attempts total** β if login still fails after the 3rd attempt, **HALT once** with a clear, actionable message: the exact `az login --tenant <tenant>` command to run, and that re-invoking the skill resumes this session (completed phases are preserved). Do NOT retry past 3 attempts, and do NOT proceed without a resolved subscription.
SKILL.md 14.0 KB
# Azure App Onboard Scaffold β IaC Generation + Self-Review
Generate deployment-ready infrastructure code from an architecture plan, verify it with adversarial self-review, and bridge to validation β all without deploying.
## Quick Reference
| Property | Value |
|----------|-------|
| Parent | [azure-app-onboard](../SKILL.md) |
| Best for | Turning `prepare-plan.json` service list into Bicep templates with secure-by-default patterns |
| Inputs | `prepare-plan.json` (services, naming, quotas), `context.json` (overrides, components, repo info) |
| Outputs | `scaffold-manifest.json`, generated IaC files in `infra/` |
| Pipeline position | Phase 3 of 4: prereq β prepare β **scaffold** β deploy |
| IaC format | Bicep (v1 default). Terraform when existing `.tf` detected or user override. |
## When to Use This Skill
Invoked by the `azure-app-onboard` orchestrator at Phase 3 when `prepare-plan.json` exists with `services[]`. Not directly user-routable in v1.
> **Return to orchestrator:** When complete, return control to `azure-app-onboard`. Do NOT directly invoke deploy β the orchestrator manages phase transitions.
## When NOT to Use
| Scenario | Use Instead |
|----------|-------------|
| User-triggered IaC (no `prepare-plan.json`) | `azure-prepare` |
| Subscription-scope landing zones | `azure-enterprise-infra-planner` |
| Execute deployment (`azd up`) | `azure-deploy` (do NOT invoke from AppOnboard pipeline) |
## MCP Tools
> See [shared tools](../references/mcp-tool-reference.md) for cross-phase tools and global parameters. See [scaffold tools](references/mcp-tools.md) for full parameter tables.
| Tool | Sub-command | Purpose | Parameters |
|------|-----------|---------|------------|
| `mcp_azure_mcp_bicepschema` | `bicepschema_get` | ARM resource type schemas | `resource_type` (Required), `api_version` (Optional) |
| `mcp_bicep_list_avm_metadata` | *(flat)* | AVM module catalog | None |
| `mcp_bicep_get_bicep_best_practices` | *(flat)* | Bicep best practices | None |
| `mcp_bicep_get_az_resource_type_schema` | *(flat)* | ARM resource type JSON schema | `azResourceType`, `apiVersion` (Required) |
| `mcp_bicep_build_bicep` | *(flat)* | Validate `.bicep` files (self-review L3) | `filePath` (Required) |
| `mcp_bicep_format_bicep_file` | *(flat)* | Format `.bicep` files (LF enforcement) | `filePath` (Required) |
| `mcp_azure_mcp_deploy` | `deploy_iac_rules_get` | IaC best practices and rules | `deployment-tool`, `iac-type`, `resource-types` |
| `mcp_azure_mcp_deploy` | `deploy_pipeline_guidance_get` | CI/CD pipeline config | `is-azd-project`, `pipeline-platform`, `deploy-option` |
| `mcp_azure_mcp_get_azure_bestpractices` | `get_azure_bestpractices_get` | SDK/Functions best practices | `resource`, `action` |
| `mcp_azure_mcp_azureterraformbestpractices` | *(flat)* | Terraform patterns (TF path only) | `resource_type` (Required) |
## Workflow
**Session folder:** `.copilot-azure/sessions/{uuid}/` β reads `prepare-plan.json` + `context.json`, writes `scaffold-manifest.json`.
### DETECT (Steps 1β4)
1. **Read `prepare-plan.json`** β verify `services[]` exists, read `naming` config (especially `naming.resourcePrefix`, `naming.suffix`, `naming.resources[]`). Read resource group name from `context.json.azure.resourceGroup`. β **Use EXACTLY these names in generated IaC β do NOT invent names, derive them from `environmentName`, or append your own suffixes.** β **Use EXACTLY the names from `prepare-plan.json.naming.resources[]` as Bicep parameters. Do NOT derive names with `take()`, `substring()`, or string manipulation. The plan is the source of truth.** Missing β trigger prepare backfill via `azure-app-onboard` orchestrator.
2. **Read `context.json`** β check `overrides[]` for `iacFormat` preference, `detectedInfra[]` for existing `.tf`, `detectedInfraProvider` for cloud provider classification.
3. **Check workspace for existing IaC** β β **Skip** if `context.json.overrides[]` contains `ignoreExistingInfra: true`. Otherwise:
- **Azure IaC** (`.bicep`, `azure.yaml`, `.tf` with `azurerm`): `ask_user` β "Start fresh" (rename `infra/` to `infra.bak/`) or "Use existing" (route to `azure-prepare`, stop pipeline).
- **Non-Azure IaC** (`.tf` with GCP/AWS): respect `context.json.overrides[].iacFormat` from prepare. Default: Bicep alongside existing TF.
- **Unknown TF** (`detectedInfraProvider.terraform` == `"unknown"`): ask user which provider before routing.
- **No IaC**: continue.
4. **Determine compute targets** β Check which compute targets are in the plan (App Service/Functions, Container Apps, or both) and whether PostgreSQL/Redis is present. Do NOT read any reference files β pass this info to the sub-agent at Step 5.
4b. **Pre-check API versions (main thread)** β MCP tool access is unreliable in `task` agents β call these in the main thread before dispatching. Call `mcp_bicep_list_az_resource_types_for_provider` (or `bicep-list_az_resource_types_for_provider`) once per provider namespace in `prepare-plan.json.services[]` (e.g., `Microsoft.Web`, `Microsoft.App`, `Microsoft.DBforPostgreSQL`, `Microsoft.Cache`, `Microsoft.KeyVault`, `Microsoft.ContainerRegistry`). Extract the latest GA API version (no `-preview`) for each resource type. Build an `apiVersions` map and pass it to the IaC gen sub-agent at Step 5. Fallback: if MCP unavailable, run `az provider show --namespace {ns} --query "resourceTypes[?resourceType=='{type}'].apiVersions[?!contains(@, 'preview')] | [0][0]" -o tsv` per resource type β this filters to GA-only and picks the latest. Pass `"MCP unavailable"` only if both MCP AND CLI fail. Sub-agent still validates generated Bicep via `az bicep build`.
### ACTION (Steps 5β12)
> β **File boundary:** NEVER modify files outside `infra/`, `.copilot-azure/`. Scaffold only writes files β no install/build commands.
> β **Sub-agent delegation is MANDATORY for Steps 5, 6β9, and 10β12.** Each step reads its `subagent-*.md` template, then dispatches a `task` call. Do NOT read any reference file not explicitly named in these steps.
>
> β **Dispatch type: `task` ONLY β NEVER `general-purpose`.** `general-purpose` leaks sub-agent context into the main thread, accelerating compaction and evicting the orchestrator workflow. `task` isolates sub-agent context.
>
> β **How to dispatch β VERBATIM COPY required:**
> 1. `view` the `subagent-*.md` template file
> 2. Your **NEXT action MUST be a `task` tool call** β not `view`, `powershell`, `create`, or ANY other tool
> 3. The task prompt MUST contain the **COMPLETE and UNMODIFIED** template text. Copy the template between `<<<TEMPLATE_START>>>` / `<<<TEMPLATE_END>>>` delimiters exactly as shown below. Do NOT summarize, paraphrase, reword, or omit ANY part of it β the sub-agent needs every "Read [file]" and "Do:" instruction to produce correct output
> 4. AFTER the template block, append the data sections (plan JSON, overrides, etc.)
>
> **Anti-pattern (causes regressions):** Writing your OWN prompt that lists workflow steps or describes what to generate. The template already contains the complete workflow β your job is to COPY it, not rewrite it.
5. **IaC generation** β β **You MUST dispatch [`subagent-iac-gen.md`](references/subagent-iac-gen.md) as a `task`.** β agent_type: `"task"` β NEVER `"general-purpose"`.
```
<<<TEMPLATE_START>>>
{paste the ENTIRE content of subagent-iac-gen.md here β unmodified}
<<<TEMPLATE_END>>>
## Data (appended by orchestrator)
### prepare-plan.json
{full JSON}
### context.json.overrides
{overrides array}
### prereq-output.json.buildRequirements
{buildRequirements object}
### prereq-output.json.warnings[]
{warnings array}
### Compute targets
{App Service/Functions, Container Apps, or both + whether PostgreSQL/Redis present}
### apiVersions
{map from Step 4b, e.g. {"Microsoft.KeyVault/vaults": "2023-07-01", ...} β or "MCP unavailable" if skipped}
### Working directory
{absolute path}
```
- **Expect:** IaC files written to `infra/`, file list returned for `scaffold-manifest.json.files[]`
- The tag `app-onboard-skill: 'true'` MUST appear verbatim in generated Bicep.
5b. **Deploy checklist (parallel with Step 5)** β Dispatch as a `task` **in parallel** with the IaC gen subagent above. β agent_type: `"task"` β NEVER `"general-purpose"`.
```
<<<TEMPLATE_START>>>
You are a deploy-checklist generator. Do NOT invoke any skills.
1. Read the deploy-checklist-template at: plugin/skills/azure-app-onboard/deploy/references/deploy-checklist-template.md
2. Fill in {placeholders} with real values from prepare-plan.json (appName, rgName, subscriptionId, sessionId).
3. Delete sections that don't apply to this deployment's compute target (e.g., remove App Service section for Container Apps deploys). The template section headers indicate which to delete.
4. Write the result to the session folder using the `create` tool. This file survives conversation compaction β deploy re-reads it after every long-running command.
<<<TEMPLATE_END>>>
## Data (appended by orchestrator)
### prepare-plan.json
{full JSON}
### Session path
{.copilot-azure/sessions/{uuid}/}
### Compute targets
{App Service, Container Apps, Static Web Apps, or combination}
```
- **Expect:** `deploy-checklist.md` written to session folder. If this subagent fails, the validate subagent (Steps 10bβ12.5) will catch the missing file.
6β9. **Self-review** β β **You MUST dispatch [`subagent-review.md`](references/subagent-review.md) as a `task`.** β agent_type: `"task"` β NEVER `"general-purpose"`.
```
<<<TEMPLATE_START>>>
{paste the ENTIRE content of subagent-review.md here β unmodified}
<<<TEMPLATE_END>>>
## Data (appended by orchestrator)
### Generated IaC files
{full content of every .bicep/.tf file}
### prepare-plan.json (services, naming, deploymentVariables)
{relevant sections}
### prereq-output.json.warnings[]
{warnings array}
```
- **Expect:** findings JSON β write to `scaffold-manifest.json.selfReview`
- FLAGGED at L1/L3 β fix IaC before proceeding
### VALIDATE β MANIFEST β APPROVE (Steps 10β12.5)
10a. **Format IaC (main thread)** β For each `.bicep` file in `infra/` (including `modules/`): call `mcp_bicep_format_bicep_file` (or `bicep-format_bicep_file`) with `{ filePath: "<absolute path>" }`.This enforces LF line endings via the `bicepconfig.json` written during IaC generation. Fallback: skip if unavailable.
10a-conf. **Conformance gate (main thread β MANDATORY for Bicep)** β β **Skip this entire step when the scaffold emitted Terraform** (`infra/main.bicep` absent) β these checks are Bicep-only (Terraform is syntax-validated via `terraform validate` in the validate subagent). Otherwise run the conformance script from this skill's `scripts/` dir; it deterministically catches ARM-rejected values `az bicep build` can't (invalid Bicep values, wrong DB version, reserved DB login, `enablePurgeProtection`):
```
{scaffoldDir}/scripts/scaffold-conformance.ps1 -SessionPath ".copilot-azure/sessions/{uuid}" -InfraPath infra # pwsh (preferred)
bash {scaffoldDir}/scripts/scaffold-conformance.sh ".copilot-azure/sessions/{uuid}" infra # bash (only if pwsh unavailable; needs jq for the plan-dependent checks)
```
β **Prefer the `.ps1` when `pwsh` is available** β it runs every check unconditionally. The `.sh` twin skips the plan-dependent checks (`DB-VERSION-MATCH`, `SERVICES-COMPLETE`, `DB-NAME-PRESENT`, `WARN-FIXED`) when `jq` is absent.
β Any BLOCK failure β fix the IaC, re-run (max 3); never present the deploy gate with an open BLOCK. Run it here in the main thread β do NOT delegate to the validate subagent or hand-judge the result when a shell exists. Pass the JSON to the validate subagent for `scaffold-manifest.json.conformance`.
10bβ12.5. **Validation + manifest** β β **You MUST dispatch [`subagent-validate.md`](references/subagent-validate.md) as a `task`.** β agent_type: `"task"` β NEVER `"general-purpose"`.
```
<<<TEMPLATE_START>>>
{paste the ENTIRE content of subagent-validate.md here β unmodified}
<<<TEMPLATE_END>>>
## Data (appended by orchestrator)
### IaC file paths
{list of generated files}
### Self-review findings (from Steps 6β9)
{findings JSON}
### prepare-plan.json
{full JSON}
### prereq-output.json.warnings[]
{warnings array}
### prereq-output.json.healthEndpoint
{detected health path string or null}
### Conformance result
{JSON from Step 10a-conf}
### Session path
{.copilot-azure/sessions/{uuid}/}
```
- **Expect:** `scaffold-manifest.json` with `validationResult`, deploy checklist generated
- Verify `deploy-checklist.md` exists (written at Step 5b) β if missing, create NOW from [`deploy-checklist-template.md`](../deploy/references/deploy-checklist-template.md). Verify `deploy-result.json` exists β if missing, create from [`deploy-schemas.ts`](../deploy/references/deploy-schemas.ts).
- β **Verify `context.json` update (main-thread β do NOT delegate).** Read `.copilot-azure/sessions/{uuid}/context.json`. If `completedPhases` does not include `"scaffold"` OR `currentPhase` is not `"deploy"`, write it yourself via `edit` / `create`: append `"scaffold"` to `completedPhases`, set `currentPhase` to `"deploy"`, update `lastModifiedUtc` to current UTC ISO 8601. This is a phase-boundary write required by [pipeline-rules.md](../references/pipeline-rules.md) β do not skip it.
- β **Return to orchestrator for Step 8 (Deploy Approval Gate).** YOUR NEXT ACTION MUST BE presenting the Deploy Gate per orchestrator SKILL.md β do NOT write a "summary of generated files" message, do NOT emit a completion report. The Deploy Gate prompt (`π Ready to deploy? ...`) is the ONLY correct next output.
## Self-Healing Loop
On validation failure β read [`scaffold-healing-rules.md`](references/scaffold-healing-rules.md) (healing cadence, PLAN_LEVEL_CHANGE, artifact consistency). Do NOT pre-read.
## Error Handling
- **Missing `prepare-plan.json`:** trigger backfill via orchestrator.
- **Existing IaC:** handled in DETECT Step 3.
- **MCP unavailable:** fall back to reference patterns, flag as "unverified."
- FLAGGED findings and healing exhaustion: see [scaffold-healing-rules.md](references/scaffold-healing-rules.md).
bicep-app-service.md 5.1 KB
# Bicep β App Service Patterns
App Service-specific Bicep patterns. For shared patterns (skeleton, naming, tags, security defaults, data modules), see [bicep-patterns.md](bicep-patterns.md).
## Module Template
Standard App Service module with managed identity and SCM/FTP auth disabled. Use this as the base for ALL App Service resources.
```bicep
param location string
param tags object
param appServicePlanId string
param appServiceName string
resource appService 'Microsoft.Web/sites@2023-12-01' = {
name: appServiceName
location: location
tags: tags
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: appServicePlanId
httpsOnly: true
siteConfig: {
minTlsVersion: '1.2'
ftpsState: 'Disabled'
}
}
}
// SCM basic auth β enabled in IaC for deploy convenience. Deploy phase re-disables via REST API after code upload.
resource scmAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2023-12-01' = {
parent: appService
name: 'scm'
properties: {
allow: true
}
}
// β MANDATORY β disable FTP basic auth
resource ftpAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2023-12-01' = {
parent: appService
name: 'ftp'
properties: {
allow: false
}
}
output appServiceId string = appService.id
output principalId string = appService.identity.principalId
```
> β **Every App Service module MUST include:** (1) `identity: { type: 'SystemAssigned' }`, (2) `scm` basicPublishingCredentialsPolicies with `allow: true` (IaC sets enabled for deploy convenience β deploy phase re-disables via REST API after code upload), (3) `ftp` basicPublishingCredentialsPolicies with `allow: false`. Missing any of these β self-review L1 `FLAGGED`.
## Native Module Deploy Strategy
When `prepare-plan.json.deployStrategy` exists with `codeDeployPattern: "startup-install"`, apply these patterns to the App Service Bicep:
```bicep
resource appService 'Microsoft.Web/sites@2023-12-01' = {
name: appServiceName
location: location
tags: tags
properties: {
serverFarmId: appServicePlan.id
httpsOnly: true
siteConfig: {
linuxFxVersion: 'NODE|${nodeVersion}' // β Use exact version tag (18-lts, 20-lts) β NEVER tilde (~18). Tilde works for WEBSITE_NODE_DEFAULT_VERSION but NOT linuxFxVersion.
ftpsState: 'Disabled'
minTlsVersion: '1.2'
// Startup command: Oryx build is primary, this is the safety-net fallback.
// Runs npm install only if node_modules doesn't exist (Oryx missed it).
// MUST be inline β never a .sh file (Windows CRLF β bash exit code 2).
appCommandLine: '${deployStrategy.startupCommand}'
appSettings: [
// Primary: tell Oryx to run npm install during zip deploy
{ name: 'SCM_DO_BUILD_DURING_DEPLOYMENT', value: 'true' }
{ name: 'ENABLE_ORYX_BUILD', value: 'true' }
// Extended timeout for native module compilation (max 1800, default 230)
{ name: 'WEBSITES_CONTAINER_START_TIME_LIMIT', value: '1800' }
// App-specific settings
{ name: 'NODE_ENV', value: 'production' }
]
}
}
}
```
**Rules:**
- β **Inline `appCommandLine` only** β never generate a `.sh` startup script file. Files created on Windows have CRLF line endings β bash exit code 2 on Linux
- β **Entry point from manifest** β read `package.json.scripts.start` or `.main`, never hardcode `index.js`
- β **`WEBSITES_CONTAINER_START_TIME_LIMIT` = 1800** (the maximum). Native compilation takes 2-5 min; Python with scipy can take longer
- When `deployStrategy` is absent (no native modules), do NOT set `appCommandLine` β let Oryx use its default startup
- β **Never prefix startup with `cd /home/site/wwwroot`** β Oryx extracts build output to a temp directory and sets the working directory automatically. Hardcoding `cd /home/site/wwwroot` causes `MODULE_NOT_FOUND` / `Could not import` because the app files aren't there
**Self-review check (L2 Pattern):** If `hasNativeModules == true`, verify Bicep has BOTH `appCommandLine` and `WEBSITES_CONTAINER_START_TIME_LIMIT`. If `prereq-output.json.initCommands[]` has `required: true` entries, verify `appCommandLine` includes them before the app start command. **FLAGGED** if either check fails.
## F1/D1 Free Tier β No Managed Identity
> β **F1/D1 does NOT support managed identity** (causes OOM / deployment failure). When the plan SKU is F1 or D1:
> - **Omit** `identity: { type: 'SystemAssigned' }` from the App Service resource
> - **Do NOT use** `@Microsoft.KeyVault()` in app settings β KV references require managed identity
> - **Instead:** Pass secrets as `@secure()` params from the KV module's `@secure()` output. The KV module generates the secret value and outputs it securely; main.bicep passes it to the App Service module as a `@secure() param`
> - **Do NOT output** `principalId` β it doesn't exist without identity
## Identity Output β SystemAssigned vs UserAssigned
> β **`appService.identity.principalId` only exists for `SystemAssigned` identity.** When using `UserAssigned`, output the managed identity MODULE's `principalId` instead β `identity.principalId` is undefined and causes `DeploymentOutputEvaluationFailed`. F1/D1 App Service: no identity (OOM), so no principalId output.
bicep-container-apps.md 9.2 KB
# Bicep β Container Apps Patterns
Container Apps-specific Bicep patterns. For shared patterns (skeleton, naming, tags, security defaults, data modules), see [bicep-patterns.md](bicep-patterns.md).
## Two-Phase Wiring
Container Apps + ACR requires two-phase deployment (circular dependency: CA needs ACR image, ACR needs CA identity for AcrPull):
1. **Phase 1:** Deploy Container App with placeholder image (`mcr.microsoft.com/azuredocs/containerapps-helloworld:latest`). β **No `registries` block, no KV `secretRef`.** The placeholder image is pulled from MCR (public). Use `registries: []` and `secrets: []`. **RBAC role assignments (AcrPull, KV Secrets User) ARE created in Phase 1** β they don't affect the placeholder deployment and need 1β2 minutes to propagate before Phase 2.
2. **Phase 2:** Build + push app image to ACR, redeploy with real image + `registries` + KV `secretRef` entries. RBAC is already propagated from Phase 1.
> β **Placeholder image listens on port 80, not your app's port.** Set `targetPort` conditionally: `var effectivePort = containerImage == 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' ? 80 : appPort`. Mismatched ports cause "Operation expired" (health probe can't reach container).
> β **`containerImage` param must exist in BOTH `main.bicep` AND the container app module.** Phase 2 passes `--parameters containerImage='...'` via CLI β if `main.bicep` lacks the param, the override is silently ignored and the placeholder persists.
```bicep
// In main.bicep: thread containerImage to module
param containerImage string = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
module containerApp './modules/containerapp.bicep' = {
params: { containerImage: containerImage /* ...other params... */ }
}
// In containerapp.bicep:
param containerImage string = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
var isPlaceholder = containerImage == 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
resource containerApp 'Microsoft.App/containerApps@2024-03-01' = {
identity: { type: 'SystemAssigned' }
properties: {
configuration: {
ingress: {
external: true
targetPort: isPlaceholder ? 80 : appPort
allowInsecure: false // β MANDATORY
}
registries: isPlaceholder ? [] : [{ server: acr.properties.loginServer, identity: 'system' }]
secrets: isPlaceholder ? [] : [ /* KV secretRefs here */ ]
}
template: {
containers: [{
image: containerImage
env: [{ name: 'PORT', value: string(isPlaceholder ? 80 : appPort) }]
}]
}
}
}
```
> β **Do NOT set `revisionSuffix`.** Omit it entirely β ARM auto-generates unique revision names. Hardcoding `revisionSuffix: 'v1'` causes Phase 2 redeploy to fail with "revision with suffix v1 already exists."
### AcrPull Role Assignment
> β **AcrPull role GUID: `7f951dda-4ed3-4680-a7ca-43fe172d538d`.** Copy verbatim β wrong GUIDs cause `RoleDefinitionDoesNotExist`.
```bicep
resource acrPullRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(acr.id, containerApp.id, '7f951dda-4ed3-4680-a7ca-43fe172d538d')
scope: acr
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
principalId: containerApp.identity.principalId
principalType: 'ServicePrincipal'
}
}
```
## Log Analytics Workspace Key
> β **Use `resource.listKeys()`, NOT `reference()`.** `reference()` does not expose `primarySharedKey`.
```bicep
// β
Correct
var laKey = logAnalyticsWorkspace.listKeys().primarySharedKey
// β Wrong
var laKey = reference(logAnalyticsWorkspace.id, '2023-09-01').primarySharedKey
```
### Log Analytics customerId vs resource ID
> β **Output BOTH `id` and `customerId` from the log-analytics module.** Container Apps Environment needs the GUID `customerId`. App Insights needs the ARM resource ID. Do NOT use `split(workspaceId, '/')[8]` β that extracts the workspace name, not the GUID.
```bicep
// log-analytics.bicep outputs:
output id string = logAnalyticsWorkspace.id // ARM resource ID
output customerId string = logAnalyticsWorkspace.properties.customerId // GUID
output sharedKey string = logAnalyticsWorkspace.listKeys().primarySharedKey
// container-app-environment.bicep:
param workspaceCustomerId string // GUID, NOT resource ID
// β MUST nest under appLogsConfiguration.destination='log-analytics' β a bare top-level logAnalyticsConfiguration fails deploy (ManagedEnvironmentInvalidSchema). This nesting is the ONLY valid location at EVERY API version (the flat shape was never valid β NOT version drift, so do not chase API-version pins). This is the CA's only log path (no diagnostic-settings module).
properties: {
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: workspaceCustomerId
sharedKey: workspaceSharedKey
}
}
}
// β WRONG: customerId: split(workspaceId, '/')[8]
```
## Ingress & Port Mapping
> β **Container resource limits:** Use decimal format for memory: `'0.5Gi'`, `'1Gi'`, `'2Gi'` β NOT Kubernetes-style `'512Mi'`. CPU must be type `string`: `'0.25'`, `'0.5'`, `'1'`. Valid combos: `0.25/0.5Gi`, `0.5/1Gi`, `0.75/1.5Gi`, `1/2Gi`, `1.25/2.5Gi`, `1.5/3Gi`, `1.75/3.5Gi`, `2/4Gi`.
> β **ACR module:** `retentionPolicy` is **Premium-only**. For Basic/Standard ACR, omit `retentionPolicy` entirely β ARM rejects it.
## Key Vault Secret References
> β **Container Apps does NOT support `@Microsoft.KeyVault(SecretUri=...)` syntax.** That is App Service-only. Container Apps uses `secretRef` with managed identity.
**Correct pattern β Container Apps secrets from Key Vault:**
> β **WRONG β `environment().suffixes.keyvaultDns` produces double-dot URL:**
> `keyVaultUrl: 'https://${kvName}${environment().suffixes.keyvaultDns}/secrets/...'`
> That function returns `.vault.azure.net` (WITH leading dot) β `kv-name..vault.azure.net` β `ContainerAppSecretKeyVaultUrlInvalid`.
> β
Use `keyVault.name` + `.vault.azure.net` (hardcoded domain) or `keyVaultModule.outputs.vaultUri`.
> β **Every `secrets[].keyVaultUrl` in a Container App MUST have a matching `Microsoft.KeyVault/vaults/secrets` child resource in the KV module.** If the CA references `sshpass` via secretRef, the KV module must create that secret. Missing secrets β `SecretNotFound` at Phase 2 deploy.
```bicep
resource containerApp 'Microsoft.App/containerApps@2024-03-01' = {
identity: {
type: 'SystemAssigned'
}
properties: {
configuration: {
secrets: [
{
name: 'db-connection-string'
// β Do NOT replace vault.azure.net with environment().suffixes.keyvaultDns β it adds a leading dot β double-dot URL
#disable-next-line no-hardcoded-env-urls
keyVaultUrl: 'https://${keyVault.name}.vault.azure.net/secrets/db-connection-string'
identity: 'system' // Uses the CA's system-assigned managed identity
}
]
}
template: {
containers: [{
env: [
{
name: 'DATABASE_URL'
secretRef: 'db-connection-string' // References the secret defined above
}
]
}]
}
}
}
```
> β **Never use conditional logic (`??`, ternary, `empty()`, `union()`) to mix plain and secret env vars in a single Bicep loop or array.** ARM evaluates ALL property paths in conditional expressions β `envVar.secretRef` errors on items that don't have that property, producing `InvalidTemplate`. Instead, define plain and secret env vars as separate arrays and concatenate:
>
> ```bicep
> env: concat(
> [
> { name: 'PORT', value: '8000' }
> { name: 'NODE_ENV', value: 'production' }
> ],
> [
> { name: 'DATABASE_URL', secretRef: 'db-connection-string' }
> { name: 'REDIS_URL', secretRef: 'redis-connection-string' }
> ]
> )
> ```
> β **KV Secrets User role scoped to Key Vault resource β NOT `resourceGroup()`.** Scoping to `resourceGroup()` causes 403.
```bicep
resource kvRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
scope: keyVault // β scope to KV resource, not RG
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') // Key Vault Secrets User
principalId: containerApp.identity.principalId // β object ID, NOT clientId
principalType: 'ServicePrincipal'
}
}
```
> β **WRONG:** `principalId: .clientId` (not the object ID) or `identity: containerApp.id` in secrets[] (use `'system'` for system-assigned MI).
> For KV secret seeding and dependency chain, see [env-var-secrets.md](env-var-secrets.md).
## Multi-Container Internal DNS
Container Apps in the same environment communicate via internal DNS: `http://{container-app-name}`. Set via env vars:
```bicep
env: [
{ name: 'API_URL', value: 'http://${apiContainerApp.name}' }
{ name: 'WORKER_URL', value: 'http://${workerContainerApp.name}' }
]
```
No ingress needed for internal-only services β set `ingress.external: false` or omit ingress entirely.
## Networking
> β **Subnets MUST be defined inline** in VNet `properties.subnets[]`, NOT as separate `Microsoft.Network/virtualNetworks/subnets` child resources. Separate child resources cause `InUseSubnetCannotBeDeleted` on redeploy when NICs are attached.
bicep-patterns-data.md 3.9 KB
# Bicep Patterns β Data Service Modules
Bicep module templates for database and cache services. Read when the prepare plan includes PostgreSQL, MySQL, or Redis.
For core patterns (file structure, skeleton, naming, tagging), see [bicep-patterns.md](bicep-patterns.md). For security defaults, see [bicep-patterns-security.md](bicep-patterns-security.md).
## PostgreSQL Flexible Server Module
```bicep
param pgName string
param location string
param tags object
param administratorLogin string
@secure()
param administratorLoginPassword string
// β @secure() β value generated ONCE at deploy time and reused on every redeploy (see deploy-checklist-template.md); never bake a value here.
param allowedExtensions string = 'uuid-ossp,pgcrypto,pg_trgm'
resource pg 'Microsoft.DBforPostgreSQL/flexibleServers@2024-08-01' = {
name: pgName
location: location
tags: tags
sku: { name: 'Standard_B1ms', tier: 'Burstable' }
properties: {
version: '16' // β use prepare-plan.json.services[].version (capabilities-verified) β do not guess
administratorLogin: administratorLogin
administratorLoginPassword: administratorLoginPassword
storage: { storageSizeGB: 32 }
}
}
// 0.0.0.0 = all Azure services (intentional) β broad access consented at the Scaffold Gate.
resource pgFirewall 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2024-08-01' = {
parent: pg
name: 'AllowAllAzureServicesAndResourcesWithinAzureIps'
properties: { startIpAddress: '0.0.0.0', endIpAddress: '0.0.0.0' }
}
// Allow PG extensions (uuid-ossp, pgcrypto, pg_trgm)
resource pgExtensions 'Microsoft.DBforPostgreSQL/flexibleServers/configurations@2024-08-01' = {
parent: pg
name: 'azure.extensions'
properties: { value: allowedExtensions, source: 'user-override' }
}
```
Wire connection string via Key Vault `secretRef` (Container Apps) or `@Microsoft.KeyVault()` (App Service).
## MySQL Flexible Server Module
```bicep
param mysqlName string
param location string
param tags object
param administratorLogin string
@secure()
param administratorLoginPassword string
// β @secure() β value generated ONCE at deploy time and reused on every redeploy (see deploy-checklist-template.md); never bake a value here.
resource mysql 'Microsoft.DBforMySQL/flexibleServers@2023-12-30' = {
name: mysqlName
location: location
tags: tags
sku: { name: 'Standard_B1ms', tier: 'Burstable' }
properties: {
version: '8.0.21' // β use prepare-plan.json.services[].version (capabilities-verified) β major-only '8.0' is rejected by ARM
administratorLogin: administratorLogin
administratorLoginPassword: administratorLoginPassword
storage: { storageSizeGB: 32 }
}
}
// 0.0.0.0 = all Azure services (intentional) β broad access consented at the Scaffold Gate.
resource mysqlFirewall 'Microsoft.DBforMySQL/flexibleServers/firewallRules@2023-12-30' = {
parent: mysql
name: 'AllowAllAzureServicesAndResourcesWithinAzureIps'
properties: { startIpAddress: '0.0.0.0', endIpAddress: '0.0.0.0' }
}
// Enforce TLS
resource mysqlTls 'Microsoft.DBforMySQL/flexibleServers/configurations@2023-12-30' = {
parent: mysql
name: 'require_secure_transport'
properties: { value: 'ON', source: 'user-override' }
}
// App database from compose (e.g. MYSQLDB_DATABASE) β emit so it exists before first boot. Omit if only the default DB is used.
resource mysqlDb 'Microsoft.DBforMySQL/flexibleServers/databases@2023-12-30' = {
parent: mysql
name: appDbName
}
```
Wire connection string via Key Vault `secretRef` (Container Apps) or `@Microsoft.KeyVault()` (App Service).
## Redis Cache Module (Minimal)
```bicep
param redisName string
param location string
param tags object
resource redis 'Microsoft.Cache/redis@2024-03-01' = {
name: redisName
location: location
tags: tags
properties: { sku: { name: 'Basic', family: 'C', capacity: 0 }, enableNonSslPort: false, minimumTlsVersion: '1.2' }
}
```
Store `redis.properties.hostName` + access key in Key Vault. Wire via `secretRef`/`@Microsoft.KeyVault()`.
bicep-patterns-security.md 7.7 KB
# Bicep Patterns β Security Defaults
Mandatory security configuration for all AppOnboard-generated Bicep. Read during IaC generation before writing resource definitions. Apply during scaffold β never defer to deploy.
For core patterns (file structure, skeleton, naming, tagging), see [bicep-patterns.md](bicep-patterns.md). For data module templates (PostgreSQL, Redis), see [subagent-iac-gen.md](subagent-iac-gen.md) Step 6.
## Key Vault Deployer RBAC
The deploying user/principal needs RBAC to write secrets (scaffold seeds initial values) and read them (verify wiring):
- **Key Vault Secrets Officer** (`b86a8fe4-44ce-4948-aee5-eccb2c155cd7`) β write secrets
- **Key Vault Secrets User** (`4633458b-17de-408a-b874-0445c86b69e6`) β read secrets (also needed by app MI)
If the app seeds data using a generated secret (admin password, API key), either display it to the user at deploy time OR ensure the deployer has read RBAC on the Key Vault.
> β **Include a role assignment for the deploying user** (`context.json.azure.userObjectId`) with Key Vault Secrets Officer scoped to the Key Vault resource. Without this, `az keyvault secret set` fails with 403 during deploy secret seeding.
## Security Defaults
> **Source:** Adapted from Azure security best practices. See [Azure security baseline](https://learn.microsoft.com/en-us/security/benchmark/azure/overview) for updates.
### Identity β Managed Identity Everywhere
> β **Managed identity decision β evaluate top to bottom, first match wins.**
>
> | Condition | Include MI? |
> |-----------|-------------|
> | F1 or D1 SKU on Linux | **NO** (MI sidecar causes OOM β use `@secure()` param + KV deployer RBAC instead) |
> | Any Key Vault, database, storage, queue, or ACR access | **YES** |
> | None of the above | **YES** (default secure) |
- **System-assigned managed identity** for all services (default). User-assigned only when shared identity is explicitly needed.
- β **Never generate `administratorLogin` or `administratorLoginPassword`** for SQL β including inside conditional branches. Use Entra-only auth (see SQL Server pattern below).
- App-to-service auth: managed identity + RBAC role assignments. Zero secrets in code or config.
```bicep
identity: {
type: 'SystemAssigned'
}
```
### SQL Server β Entra-Only Authentication
> For full SQL auth reference (connection strings, managed identity SQL grants, CI/CD principal types), see `azure-prepare/references/services/sql-database/auth.md`.
```bicep
param principalId string
param principalName string
@allowed(['User', 'Group', 'Application'])
param principalType string = 'User'
// Preview API required β azureADOnlyAuthentication via administrators block
// is not available in GA API versions (GA path uses a separate child resource).
resource sqlServer 'Microsoft.Sql/servers@2024-05-01-preview' = {
name: '${resourcePrefix}-sql-${uniqueHash}'
location: location
properties: {
administrators: {
administratorType: 'ActiveDirectory'
principalType: principalType
login: principalName
sid: principalId
tenantId: subscription().tenantId
azureADOnlyAuthentication: true
}
minimalTlsVersion: '1.2'
}
}
```
> β οΈ If deploying from CI/CD with a service principal, set `principalType` to `'Application'`. The default `'User'` only works for interactive deployments.
### Secrets β Key Vault References
Store secrets in Key Vault. Reference via app settings β never inline.
> β **No plaintext secrets in Bicep `appSettings`.** Values like `SECRET_KEY`, `JWT_SECRET`, `API_KEY`, session secrets, and database passwords MUST NOT be hardcoded β not even as placeholders. Never use `uniqueString()` for secrets (deterministic/predictable). These appear in ARM deployment history and persist in source control.
>
> **Container Apps exception:** Phase 1 of two-phase deployment uses `secrets: []` β NO secrets at all (not plaintext, not KV). KV `secretRef` entries are activated in Phase 2 after RBAC propagates. See [bicep-container-apps.md](../../scaffold/references/bicep-container-apps.md) Β§ Two-Phase Wiring.
>
> β **Container Apps KV URL β do NOT use `environment().suffixes.keyvaultDns`.** That function returns `.vault.azure.net` (WITH leading dot) β double-dot URL β `ContainerAppSecretKeyVaultUrlInvalid`. Use `'https://${kvName}.vault.azure.net/secrets/...'` with `#disable-next-line no-hardcoded-env-urls` to suppress the linter.
>
> **Correct patterns:**
> 1. **Key Vault reference (preferred):** `'@Microsoft.KeyVault(VaultName=${kvName};SecretName=secret-key)'`
> 2. **Deploy-time seeding (free-tier):** Omit from Bicep; run `az webapp config appsettings set --settings SECRET_KEY=$(openssl rand -base64 32)` post-deploy
> 3. **Bicep `@secure()` parameter:** Pass via CLI `--parameters secretKey=$(openssl rand -base64 32)` β never committed to parameters.json
>
> β **NEVER:** `{ name: 'SECRET_KEY', value: 'hard-to-guess-string' }` or `value: 'change-me'` in Bicep
```bicep
// App Service / Functions β Key Vault reference pattern
appSettings: [
{
name: 'DB_CONNECTION_STRING'
value: '@Microsoft.KeyVault(VaultName=${kvName};SecretName=db-connection-string)'
}
]
```
Key Vault module β emit this resource EXACTLY; add no other properties. `enablePurgeProtection` is deliberately absent (ARM rejects `false`; `true` blocks cleanup).
```bicep
resource kv 'Microsoft.KeyVault/vaults@{apiVersion}' = {
name: kvName
location: location
tags: tags
properties: {
sku: { family: 'A', name: 'standard' }
tenantId: subscription().tenantId
enableRbacAuthorization: true // RBAC, not access policies
enableSoftDelete: true
softDeleteRetentionInDays: 7
networkAcls: { defaultAction: 'Allow', bypass: 'AzureServices' }
}
}
```
### Transport β HTTPS Only
All web-facing resources:
```bicep
// App Service
httpsOnly: true
siteConfig: {
minTlsVersion: '1.2'
}
// Storage
supportsHttpsTrafficOnly: true
allowBlobPublicAccess: false
minimumTlsVersion: 'TLS1_2'
```
### App Service / Functions β Publishing Credential Lockdown
> β **Every App Service and Functions app MUST include both `basicPublishingCredentialsPolicies` child resources.** Missing these means deploy cannot toggle SCM auth post-deployment β the REST API call targets a resource that doesn't exist in ARM.
```bicep
// SCM β allow: true for deploy phase (deploy re-disables via REST API after code upload)
resource scmAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2023-12-01' = {
parent: appService
name: 'scm'
properties: {
allow: true
}
}
// FTP β always disabled
resource ftpAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2023-12-01' = {
parent: appService
name: 'ftp'
properties: {
allow: false
}
}
```
> **Deploy lifecycle:** Scaffold sets `scm.allow: true` so `az webapp deploy` works. After code upload + health check, deploy phase runs `az rest --method put .../basicPublishingCredentialsPolicies/scm` with `allow: false` to re-harden. If scaffold omits these resources, deploy's Step 7 SCM re-disable REST API call fails silently.
### Cosmos DB β Data Plane RBAC
β Cosmos DB uses its own role system β see [rbac-roles.md](rbac-roles.md) Β§ Cosmos DB for role IDs and behavioral rules. Do NOT use `Microsoft.Authorization/roleAssignments` for Cosmos data access.
### RBAC β Deterministic Role Assignments
For the common roles GUID table, see [rbac-roles.md](rbac-roles.md).
```bicep
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(scopeResourceId, principalId, roleDefinitionId)
scope: targetResource
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId)
principalId: managedIdentity.properties.principalId
principalType: 'ServicePrincipal' // REQUIRED β prevents AAD graph lookup delays
}
}
bicep-patterns.md 6.7 KB
# Bicep Patterns
Bicep default-path patterns for AppOnboard scaffold. Used as the primary IaC format. For the alternative Terraform path (existing `.tf` files or user override), scaffold uses `mcp_azure_mcp_azureterraformbestpractices` output patterns.
> **Source:** Adapted from Azure Bicep best practices. See [Bicep best practices](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/best-practices) for updates.
## File Structure
> β **Always use `targetScope = 'subscription'`.** Subscription-scope Bicep creates the resource group in IaC with all 5 AppOnboard tags (including `created-at`). Resource-group scope requires `az group create` via CLI, which consistently misses `created-at` because CLI-created resource groups don't receive IaC-managed tags. There are zero benefits to resource-group scope for AppOnboard.
```
infra/
βββ main.bicep # Entry point (subscription scope)
βββ main.parameters.json # ARM JSON parameter values (NOT .bicepparam)
βββ modules/
βββ container-app.bicep
βββ app-service.bicep
βββ sql-database.bicep
βββ key-vault.bicep
βββ log-analytics.bicep
βββ ...
```
Each service gets its own module. `main.bicep` orchestrates resource group creation + module calls.
## main.bicep Skeleton
```bicep
targetScope = 'subscription'
@minLength(1)
@maxLength(64)
param environmentName string
@minLength(1)
param location string
param sessionId string
param deployedBy string // resolved via: az ad signed-in-user show --query displayName -o tsv
// β createdAt: passed in parameters.json, NOT utcNow() default (crashes Portal blade)
param createdAt string
var tags = {
'app-onboard-skill': 'true'
'app-onboard-session-id': sessionId
'created-at': createdAt
environment: environmentName
'deployed-by': deployedBy
}
resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = {
name: 'rg-${environmentName}'
location: location
tags: tags
}
// β scope: rg (symbolic) β creates implicit dependsOn. Do NOT use resourceGroup(name) β it races against RG creation.
module resources './modules/resources.bicep' = {
name: 'resources'
scope: rg
params: {
location: location
environmentName: environmentName
tags: tags
}
}
```
## main.parameters.json
> β **ARM JSON only.** Do NOT use `.bicepparam` syntax (`using`, `param`, `readEnvironmentVariable()`). AppOnboard deploys via `az deployment sub create` (subscription-scope default) β not `azd` β and `.bicepparam` requires azd or newer tooling. If the user lacks subscription-level permissions, the deploy phase falls back to `az deployment group create` automatically.
```json
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environmentName": { "value": "{project}-{env}" },
"location": { "value": "{region}" },
"sessionId": { "value": "{context.json.sessionId}" },
"deployedBy": { "value": "{context.json.azure.userDisplayName}" }
}
}
```
## Naming Convention (Bicep)
The prepare phase generates a logical resource prefix in `prepare-plan.json.naming.resourcePrefix` (e.g., `myapp-dev`). Scaffold MUST add a globally unique suffix using Bicep's `uniqueString()` function to prevent cross-deployment name collisions on globally unique Azure resources (App Service, Key Vault, Storage Account, ACR).
```bicep
// main.bicep β derive unique suffix from resource group
var nameSuffix = uniqueString(resourceGroup().id)
// Pass unique names to modules
param kvName string = 'kv-${resourcePrefix}-${take(nameSuffix, 4)}'
param appName string = 'app-${resourcePrefix}-${take(nameSuffix, 4)}'
param storName string = 'st${replace(resourcePrefix, '-', '')}${take(nameSuffix, 4)}'
param acrName string = 'cr${replace(resourcePrefix, '-', '')}${take(nameSuffix, 4)}'
```
> β **Do NOT use `uniqueString()` for secrets** β it is deterministic and predictable. See [bicep-patterns-security.md](bicep-patterns-security.md) Β§ Secrets for correct secret patterns.
If `prepare-plan.json.naming.resources[]` provides pre-computed names with suffixes, prefer those β but ALWAYS ensure globally unique resources include a `uniqueString()` or equivalent hash in main.bicep as a safety net.
## Log Analytics Module Output
> β **Output the resource ID (`.id`), NOT `.properties.customerId`.** Container Apps Environment requires `workspaceResourceId` (the full ARM resource ID). `.properties.customerId` is the GUID used for queries β passing it as `workspaceResourceId` causes an ARM deploy failure (`BadRequest`). Separate the two outputs:
> ```bicep
> output workspaceId string = logAnalyticsWorkspace.id // ARM resource ID β for CAE, App Insights
> output workspaceCustomerId string = logAnalyticsWorkspace.properties.customerId // GUID β for Log Analytics queries only
> ```
## Compute-Target Patterns
Read the file(s) matching the service mapping β load only what's needed:
- **App Service / Functions:** [bicep-app-service.md](bicep-app-service.md) β module template, SCM/FTP auth, native module deploy strategy
- **Container Apps:** [bicep-container-apps.md](bicep-container-apps.md) β two-phase ACR wiring, ingress, secretRef, image parameter, multi-container DNS
- **Static Web Apps:** [bicep-swa.md](bicep-swa.md) β module template, detached deploy rule
Load multiple only if the plan includes multiple compute targets.
> β **F1/D1 SKU: do NOT generate a Dockerfile.** If `prepare-plan.json` specifies F1 or D1 (free/shared tier), use the platform's built-in runtime stack (e.g., `NODE|24-lts` for Node.js, `PYTHON|3.14` for Python). Dockerfiles are for B1+ or Container Apps only.
> β **Native module deploy strategy.** If `prepare-plan.json.deployStrategy` exists, read [bicep-app-service.md Β§ Native Module Deploy Strategy](bicep-app-service.md) and apply the startup command + app settings. `deployStrategy.startupCommand` β `appCommandLine`, `deployStrategy.requiredAppSettings` β `appSettings[]`. When no `deployStrategy` exists, do NOT set `appCommandLine`.
## Service Tagging
> β **You MUST read [iac-generation-rules.md Β§ Session Tags](iac-generation-rules.md).** All resources MUST include the 5 AppOnboard session tags. Pass `tags` object from `main.bicep` into every module.
## API Version Policy
Use the latest stable API version for each resource type. Never use preview APIs unless required for a feature with no GA alternative. Validate via `bicep build` β stale API versions produce warnings.
## Key Vault Reference Syntax
Syntax differs by service β never mix. See [bicep-container-apps.md](bicep-container-apps.md) for Container Apps `secretRef` and [bicep-patterns-security.md](bicep-patterns-security.md) for App Service `@Microsoft.KeyVault()`. For Terraform, see [terraform-patterns.md](terraform-patterns.md).
bicep-swa.md 0.7 KB
# Bicep β Static Web Apps Patterns
SWA-specific Bicep patterns. For shared patterns (skeleton, naming, tags, security defaults, data modules), see [bicep-patterns.md](bicep-patterns.md).
## Module Template
SWA modules for token-based deploys (no GitHub CI/CD):
```bicep
resource staticWebApp 'Microsoft.Web/staticSites@2023-12-01' = {
name: swaName
location: location
tags: tags
sku: { name: 'Free', tier: 'Free' }
properties: {} // β MUST be empty β no repositoryUrl, no branch, no buildProperties
}
```
> β **Detached SWA deploy:** Omit `repositoryUrl`, `branch`, and `buildProperties` entirely. These are only for GitHub Actionsβconnected deployments. Including `repositoryUrl: ''` causes `BadRequest: RepositoryUrl is invalid`.
cicd-pipelines.md 0.3 KB
# CI/CD Pipeline Patterns
CI/CD is deferred to v2. Do NOT auto-generate workflow files.
If the user requests CI/CD guidance, call `mcp_azure_mcp_deploy` β `deploy_pipeline_guidance_get` with `is-azd-project: false`, `pipeline-platform: 'github-actions'`, `deploy-option: 'provision-and-deploy'` and present the guidance.
dockerfile-generation.md 3.8 KB
# Dockerfile Generation
Generate Dockerfiles for components targeting Container Apps (or B1+ App Service) that have no existing Dockerfile. Read this when `iac-generation-rules.md` Step 6b applies.
## When to Generate
- Component targets Container Apps and has NO Dockerfile (and no `Dockerfile.azure`)
- Component targets App Service B1+ with `deployStrategy.containerized: true`
- β Do NOT generate for F1/D1 SKUs β use platform runtime stack instead
- β Do NOT overwrite an existing Dockerfile β create `Dockerfile.azure` only if BuildKit stripping is needed
## Principles
### Layer ordering
Copy dependency manifests and install BEFORE copying source (preserves layer cache):
```
COPY {manifest files} ./
RUN {install command}
COPY . .
```
### Base image selection
| Principle | Rule |
|-----------|------|
| Pin version | `node:{major}-slim`, NOT `node:latest` |
| Slim variants | `-slim` or `-alpine` for smaller images |
| Multi-stage | Go, .NET, Java, Rust: build in SDK image, copy binary to runtime |
| Match runtime | Read `engines`, `python_requires`, `go.mod`, `<TargetFramework>` |
### Port alignment
`EXPOSE` port, app's listening port, and Container App `targetPort` must all match. Mismatch = silent health probe failure. Read app config for listening port, set `EXPOSE {port}`, add `ENV PORT={port}` if app reads `PORT` from env.
### Security defaults
- Non-root user β Debian/`-slim` base: `RUN groupadd -r app && useradd -r -g app app`; Alpine base: `RUN addgroup -S app && adduser -S app -G app`. Then `USER app`
- Never `COPY .env` or secrets β use `.dockerignore`
- Direct exec: `CMD ["node", "server.js"]` not `CMD ["npm", "start"]`
### .dockerignore
Always generate alongside Dockerfile. Exclude: `.git`, `node_modules`, `__pycache__`, `*.pyc`, `.env`, `.env.*`, `.azure`, `.copilot-azure`, `infra`, `*.md`.
### Common pitfalls
| Mistake | Fix |
|---------|-----|
| `COPY . .` before deps | Copy manifests first, install, then source |
| `npm install` in prod | `npm ci --omit=dev` |
| Wrong EXPOSE port | Read actual listening port from app source |
### Next.js multi-container build args
`NEXT_PUBLIC_*` env vars are embedded in the client JS bundle at `npm run build` time β runtime Container App env vars have zero effect on client-side code. For multi-container deploys where a Next.js frontend references another component's API:
1. Dockerfile MUST include `ARG NEXT_PUBLIC_API_URL` before the `RUN npm run build` step
2. Deploy phase passes `--build-arg NEXT_PUBLIC_API_URL=https://{api-fqdn}` to `az acr build`
Detect from `.env*` files containing `NEXT_PUBLIC_*` pointing to another service (e.g., `NEXT_PUBLIC_API_URL=http://localhost:3001`).
### ACR Build Compatibility β `Dockerfile.azure` Generation
β ACR `az acr build` uses the **classic Docker builder β NOT BuildKit**. Do NOT assume ACR supports BuildKit.
**When to generate `Dockerfile.azure`:** If `buildRequirements.hasBuildKitSyntax == true` OR the existing Dockerfile contains any BuildKit-only syntax, create `{component}/Dockerfile.azure` with all BuildKit syntax removed.
**BuildKit-only syntax** (strip all): `# syntax=` directives, `RUN --mount=...` (all types: cache, secret, bind, tmpfs), `RUN --network=...`, `RUN --security=...`, `COPY --link`, `COPY --chmod=...`, heredoc syntax (`RUN <<EOF`).
β **Package manager pinning applies here too.** When stripping BuildKit from an existing Dockerfile, also replace `npm install -g {pm}@latest` with the exact version from the project's `packageManager` field in `package.json` (e.g., `pnpm@9.4.0`). The upstream Dockerfile's `@latest` may pull a version incompatible with the base image's Node.js version.
β Handle multi-line continuations (`\`) β remove the BuildKit flag but preserve the actual command across all continuation lines. Never leave a bare `RUN` with no command.
env-var-secrets.md 4.1 KB
# Environment Variables & Secrets β Cross-Cutting Rules
Applies to ALL compute targets (App Service, Container Apps, Functions). For Container Apps-specific Bicep patterns (secretRef, identity), see [bicep-container-apps.md](bicep-container-apps.md).
## Environment Variable Value Derivation
> β **Never invent env var values β derive from the app's config class.** Cross-reference `.env.example`, `.env.sample`, `docker-compose.yml`, and the app's config module. Verify each value:
> 1. **Type validation:** URL-typed fields need valid URLs β not `*` or placeholders
> 2. **Defaults:** Use app defaults unless overriding with deployed URL
> 3. **Required:** Fields without defaults must be provided
>
> **Pitfalls:** `CORS_ORIGINS=["*"]` β invalid for strict validators (use actual URLs). `DATABASE_URL=changethis` β use KV ref. JSON array env vars need Bicep variable escaping:
> ```bicep
> var corsOrigins = '["https://${containerApp.properties.configuration.ingress.fqdn}"]'
> { name: 'CORS_ORIGINS', value: corsOrigins }
> ```
## Key Vault Secret Dependency Chain
> β **Chicken-and-egg:** CA references KV secrets that don't exist yet at deploy time. KV secret values (DB connection strings, passwords) are only known AFTER IaC creates the database.
**Correct ordering:**
1. **IaC Phase 1:** Key Vault β RBAC β Database β Container App with `secrets: []` (placeholder image, no KV refs yet)
2. **Deploy phase seeds KV:** `az keyvault secret set --vault-name {kv} --name db-connection-string --value {value}`
3. **IaC Phase 2:** Redeploy Bicep with `isPlaceholder=false` β activates KV `secretRef` entries + real image + ACR registries
**IaC pattern β reference secrets by name, gated by `isPlaceholder`:**
> β **Container Apps:** KV `secretRef` entries MUST be gated behind `isPlaceholder` (see [bicep-container-apps.md](bicep-container-apps.md) Β§ Two-Phase Wiring). Phase 1 deploys with `secrets: []` because the CA's managed identity has no RBAC yet. Phase 2 activates KV refs after RBAC propagates.
```bicep
// Phase 1: secrets: [] (isPlaceholder == true)
// Phase 2: KV secretRef entries activated after RBAC propagates
secrets: isPlaceholder ? [] : [
{
name: 'db-connection-string'
keyVaultUrl: 'https://${keyVault.name}.vault.azure.net/secrets/db-connection-string'
identity: 'system'
}
]
```
> β **Do NOT hardcode secrets in committed files** β not in `main.parameters.json`, `terraform.tfvars`, env vars, or any generated file. (`@secure()` Bicep params ARE the correct way to pass a secret at deploy time β the ban is on committing the value, not on the parameter.) The deploy phase seeds secrets into Key Vault via `az keyvault secret set` after database provisioning β see [code-deployment-appservice.md](../../deploy/references/code-deployment-appservice.md) or [code-deployment-container-apps.md](../../deploy/references/code-deployment-container-apps.md) Β§ Database Post-Deploy.
## Azure Managed Service SSL/TLS Requirements
> β **Azure managed databases and caches enforce TLS. Local docker-compose configs typically don't.** This mismatch causes container crashes post-deploy.
Check `prereq-output.json.warnings[]` for warnings with `fixPhase: "scaffold"`. Each warning's `fix` field describes the required IaC change. Read the app's config loader for the actual env var name.
> β **Prefer env var override over code change.** Only modify source if no env override path exists AND user approves.
> β **Self-review:** If any `fixPhase: "scaffold"` warning exists and IaC lacks the fix β flag as FLAGGED.
## Key Vault Secret Naming
> β **KV secret names allow only alphanumeric characters and hyphens.** Map env var names: `SECRET_KEY` β `secret-key`, `DATABASE_URL` β `database-url`. Do NOT use underscores β Azure rejects them with `SecretNameInvalid`.
## Compose β Azure PaaS Credential Mapping
> β **Azure managed databases only create the `administratorLogin` user.** Docker-compose `POSTGRES_USER` / `MYSQL_USER` auto-creates a database user β Azure PostgreSQL/MySQL Flexible Server does NOT. Map compose user env vars to the `administratorLogin` value from your Bicep, not the compose username.
error-handling.md 1.5 KB
# Error Handling β Scaffold Sub-Skill
| Error | Remediation |
|-------|-------------|
| `prepare-plan.json` missing | Trigger prepare backfill via `azure-app-onboard` orchestrator. Do not generate IaC without a plan. |
| Existing Azure IaC (`.bicep`, `azure.yaml`, or `.tf` with `azurerm` provider) | β Never delete/overwrite; move to `.copilot-azure/sessions/<id>/replaced-files/` (mirror path), tell the user their original was preserved at that backup location, then scaffold. |
| Existing non-Azure IaC (`.tf` with GCP/AWS provider) | Generate Azure TF alongside β see [terraform-patterns.md Β§ Non-Azure IaC coexistence](terraform-patterns.md). Do NOT halt. |
| MCP tool unavailable | Fall back to reference patterns. Flag generated IaC as "unverified against best practices." |
| Self-review finds FLAGGED items | Include in `scaffold-manifest.json.selfReview.findings[]`. Surface at approval gate. |
| Self-healing exhausted (3 attempts) | Pause auto-healing. Present diagnosis: (1) explain error pattern, (2) propose specific next fix, (3) ask user: "Yes, try that" / "I have a suggestion" / "Stop." If user continues, auto-heal for 5 more, then ask every 5 thereafter. If user stops, write `validationResult` with `status: "Failed"` and all errors. Do NOT proceed to deploy. |
| Schema summary exceeds token limit | Use sub-agent pattern: compress each schema to β€500 tokens. |
| `context.json` malformed | Halt. Report: "Session state corrupted β consider starting a fresh session." |
iac-generation-rules.md 6.5 KB
# IaC Generation Rules β Steps 5β8
Rules for generating infrastructure code, Dockerfiles, security verification, and telemetry wiring.
## Step 5 β Generate IaC
For each service in `services[]`:
> **Sub-agent delegation:** Use [subagent-iac-gen.md](subagent-iac-gen.md) template verbatim. Include full `prepare-plan.json`, `ScaffoldManifest` interface, and compute-target patterns. Self-review (Step 9) remains mandatory after sub-agent returns.
> **PostgreSQL wiring:** Include firewall rule + extension allow-list (see [subagent-iac-gen.md](subagent-iac-gen.md) Step 6 if PostgreSQL in plan). β **PostgreSQL config resources** (e.g., `require_secure_transport`) must use `source: 'user-override'` β `'system-default'` is read-only and ARM rejects it. β **Do NOT create `databases/postgres` child resource** β it exists by default and ARM rejects duplicate creation. **BuildKit Dockerfiles:** Generate `Dockerfile.azure` for ACR compatibility β see [code-deployment-container-apps.md Β§ BuildKit](../../deploy/references/code-deployment-container-apps.md).
> **Env var completeness:** Read `.env.example` (or `.env.sample`, `config.example`) + config/settings files (Pydantic `Settings`, `@t3-oss/env-nextjs`, Django `settings.py`) for each component to enumerate required env vars before generating IaC. Every env var with a placeholder value (not `localhost`) should map to either: (1) a Bicep parameter, (2) a KV secret reference, or (3) a hardcoded value derived from other resources (e.g., database connection string from the DB module output). β **Container Apps:** KV `secretRef` entries must be gated behind `isPlaceholder` β Phase 1 = `secrets: []`, Phase 2 activates KV refs. See [bicep-container-apps.md](bicep-container-apps.md). Flag unmapped vars in selfReview as β οΈ WARN. Missing vars cause container crash loops at deploy time.
> β **Set `targetScope = 'subscription'` in `main.bicep`.** Subscription-scope Bicep creates the resource group in IaC with all 5 AppOnboard tags (including `created-at`). Do NOT use default resource-group scope β it requires imperative `az group create` which consistently misses tags. If the user lacks subscription-level permissions, the deploy phase handles fallback to RG-scope automatically.
> β **Native module deploy strategy.** If `prepare-plan.json.deployStrategy` exists, read [bicep-app-service.md Β§ Native Module Deploy Strategy](bicep-app-service.md) and apply the startup command + app settings to the App Service Bicep. The `deployStrategy.startupCommand` goes into `appCommandLine`, and `deployStrategy.requiredAppSettings` goes into `appSettings[]`. When no `deployStrategy` exists, do NOT set `appCommandLine` β let Oryx use its default startup.
### Session Tags β Mandatory on ALL Resources
β Include the 5 AppOnboard tags on every resource and module. See [bicep-patterns.md Β§ tags](bicep-patterns.md) for Bicep code block or [terraform-patterns.md Β§ tags](terraform-patterns.md) for HCL code block.
| Tag key | Value source |
|---------|-------------|
| `app-onboard-skill` | `'true'` |
| `app-onboard-session-id` | `sessionId` param |
| `created-at` | ISO timestamp (scaffold populates, deploy may override) |
| `environment` | `naming.resourcePrefix` |
| `deployed-by` | `context.json.azure.userDisplayName` |
If extending existing IaC, MERGE with existing tags using `union()` / `merge()`.
> β **Do NOT generate `azure.yaml`.** Deploy via `az deployment sub create`. See [pipeline-rules.md](../../references/pipeline-rules.md) Β§ azure.yaml prohibition.
### Verification
β **API version verification:** Use versions from `apiVersions` input map. If a type is missing from the map, use the latest GA version from reference file examples β no `-preview` suffix. `az bicep build` catches invalid versions at compile time.
β **Resource property verification:** Training data references deprecated properties. Known traps: `Microsoft.CognitiveServices/accounts/deployments` uses `sku` (name + capacity), NOT `scaleSettings` (deprecated) β omit `raiPolicyName`; Key Vault `enablePurgeProtection` β omit entirely (`false` rejected by ARM, `true` blocks cleanup). Fallback: `az bicep build` + `what-if`.
### Output
`infra/main.bicep`, `main.parameters.json`, `modules/{service}.bicep` per service.
### Platform Compatibility
- **Line endings:** Generated `.bicep` files need LF, not CRLF β Bicep triple-quoted strings pass content literally to ARM, and `\r` bytes crash `/bin/sh` in containers. The validate subagent runs `mcp_bicep_format_bicep_file` (or `bicep-format_bicep_file`) post-generation to enforce this. If the formatter is unavailable, ensure files use LF manually.
- **Shell compatibility:** Startup scripts in Bicep multiline strings MUST use `set -eu` (POSIX). Do NOT use `set -euo pipefail` β Container Apps base images use `/bin/sh` (dash), not bash.
- **Package manager pinning:** When generating or modifying Dockerfiles, pin package manager versions from the project's `packageManager` field (e.g., `pnpm@9.4.0`). Never use `@latest` β major version drift breaks builds on older Node.js base images.
### Security Patterns β Apply During Generation
β Apply ALL patterns from [`bicep-patterns-security.md`](bicep-patterns-security.md) during generation β managed identity, SCM/FTP auth policies, KV secrets, least-privilege RBAC.
## Step 6b β Dockerfile Generation (conditional)
If `prepare-plan.json.services[]` has any entry with `name` containing "Container Apps" AND that component has no existing Dockerfile β β **You MUST read [`dockerfile-generation.md`](dockerfile-generation.md)** and generate one. Skip if all Container Apps components already have Dockerfiles, or if no service targets Container Apps.
## Step 7 β Secure-by-Default Verification
β Read [`bicep-patterns-security.md`](bicep-patterns-security.md) (or `terraform-patterns.md` Β§ Security Defaults) and verify ALL security patterns from Step 5. If deployment includes compute with MI β resource RBAC, β read [rbac-roles.md](rbac-roles.md) for GUID table. **If a role is NOT in the table, check [Azure built-in roles docs](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles).** Never guess GUIDs. Skip RBAC for SWA-only.
Tell the user which patterns were applied: managed identity, KV secrets, least-privilege RBAC, SCM/FTP auth policies, private endpoints.
## Step 8 β Wire Telemetry
If `prepare-plan.json.instrumentation.appInsightsEnabled` is `true`, add `APPLICATIONINSIGHTS_CONNECTION_STRING` as a plain app setting wired from the App Insights module output. The connection string is not a secret β no KV storage needed. For Container Apps: use a plain env var (not `secretRef`).
mcp-tools.md 6.9 KB
# Scaffold Phase β MCP Tools
Phase-exclusive tool parameters for the scaffold phase. For shared tools (`get_azure_bestpractices`, `subscription_list`, `group_list`, `extension_cli_install`), see [mcp-tool-reference.md](../../references/mcp-tool-reference.md).
> **Troubleshooting:** If a tool call fails with unknown parameter or missing command errors, consult the official docs: <https://learn.microsoft.com/en-us/azure/developer/azure-mcp-server/tools/>
---
## Azure MCP Tools β `mcp_azure_mcp_*`
### `mcp_azure_mcp_deploy` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `deploy_plan_get` | `workspace-folder`, `project-name`, `target-app-service` (ContainerApp\|WebApp\|FunctionApp\|AKS), `provisioning-tool` (AzCli\|AZD), `source-type` (from-project\|from-azure\|from-context), `deploy-option` (provision-and-deploy\|deploy-only\|provision-only) | `iac-options` (bicep\|terraform), `resource-group` | β
|
| `deploy_iac_rules_get` | `deployment-tool` (AzCli\|AZD) | `iac-type` (bicep\|terraform), `resource-types` (comma-sep: appservice, containerapp, function, aks, azuredatabaseforpostgresql, azuredatabaseformysql, azuresqldatabase, azurecosmosdb, azurestorageaccount, azurekeyvault) | β
|
| `deploy_pipeline_guidance_get` | `is-azd-project` (bool), `pipeline-platform` (github-actions\|azure-devops), `deploy-option` (deploy-only\|provision-and-deploy) | subscription, tenant | β
|
### `mcp_azure_mcp_bicepschema` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `bicepschema_get` | `resource_type` (e.g. Microsoft.Web/sites) | `api_version` | β
|
**Usage:** ARM resource type schemas. Primary tool for scaffold β returns resource properties, required fields, and constraints. Prefer over `mcp_bicep_get_az_resource_type_schema` when you need a quick schema lookup without specifying an exact API version.
### `mcp_azure_mcp_azureterraformbestpractices` (flat)
| Required | Optional | Read-Only |
|----------|----------|-----------|
| *(none)* | `resource_type` (e.g. "azurerm_linux_web_app") | β
|
**Usage:** Returns Terraform best practices for Azure resources. Omit `resource_type` for general guidance. Provide `resource_type` to get resource-specific `azurerm` configuration patterns, recommended properties, and security settings. Used during scaffold Step 5 for per-resource HCL generation (Terraform path only).
---
## Bicep MCP Tools β `mcp_bicep_*`
### `mcp_bicep_get_bicep_best_practices`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| *(none)* | β | β
|
Returns: comprehensive best practices for Bicep authoring (naming, organization, parameters, security, testing).
### `mcp_bicep_get_az_resource_type_schema`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| `azResourceType` (e.g. Microsoft.KeyVault/vaults), `apiVersion` (e.g. 2024-11-01) | β | β
|
Returns: complete JSON schema for the resource type including all properties, nested types, constraints.
### `mcp_bicep_list_avm_metadata`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| *(none)* | β | β
|
Returns: metadata for all Azure Verified Modules (AVM) including versions and docs.
### `mcp_bicep_list_az_resource_types_for_provider`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| `providerNamespace` (e.g. Microsoft.Compute) | β | β
|
Returns: all resource types and API versions for the provider.
### `mcp_bicep_build_bicep`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| `filePath` (absolute) | β | β
|
Returns: compiled ARM template JSON + compilation errors, warnings, info for a `.bicep` file.
### `mcp_bicep_format_bicep_file`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| `filePath` (absolute) | β | β |
Formats the Bicep file per official standards. Respects `bicepconfig.json`.
### `mcp_bicep_get_deployment_snapshot`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| `filePath` (absolute, `.bicepparam`) | β | β
|
Returns: deployment snapshot from a Bicep parameters file β resolves all parameter values and template references into a single deployable payload. Useful for pre-deploy validation.
### `mcp_bicep_get_file_references`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| `filePath` (absolute, `.bicep` or `.bicepparam`) | β | β
|
Returns: all file references (modules, parameters, imports) from a Bicep file. Useful for dependency analysis and verifying scaffold output completeness.
### `mcp_bicep_decompile_arm_template_file`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| `filePath` (absolute, .json/.jsonc/.arm) | β | β |
Converts ARM JSON β Bicep. Best-effort; may need manual review.
### `mcp_bicep_decompile_arm_parameters_file`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| `filePath` (absolute, .json/.jsonc/.arm) | β | β |
Converts ARM parameters JSON β `.bicepparam`.
---
## Phase 3 Tool Map
| Tool | Sub-command | AppOnboard Step | Purpose |
|------|-----------|----------|---------|
| `mcp_bicep_get_bicep_best_practices` | *(flat)* | Step 5 | **Primary** β Bicep conventions, naming, module patterns for IaC generation |
| `mcp_bicep_get_az_resource_type_schema` | *(flat)* | Step 5 | **Primary** β ARM resource type JSON schema per service for property validation |
| `mcp_bicep_list_avm_metadata` | *(flat)* | Step 5 | **Primary** β AVM module catalog; prefer verified modules over raw resource definitions |
| `mcp_azure_mcp_bicepschema` | `bicepschema_get` | Step 5 | **Primary** β ARM resource type schemas without requiring exact API version |
| `mcp_azure_mcp_deploy` | `deploy_iac_rules_get` | Step 5 | IaC best practices and rules. `--iac-type bicep` (default) or `--iac-type terraform` |
| `mcp_azure_mcp_get_azure_bestpractices` | `get_azure_bestpractices_get` | Step 5 | SDK patterns during IaC gen. `resource: "general"`, `action: "code-generation"` |
| `mcp_bicep_build_bicep` | *(flat)* | Step 9 | **Primary** β validate generated `.bicep` files in self-review L3 (syntax, properties, API versions) |
| `mcp_bicep_format_bicep_file` | *(flat)* | Step 9 | Format generated `.bicep` files per official standards after self-review fixes |
| `mcp_bicep_get_file_references` | *(flat)* | Step 11 | Verify scaffold output completeness β all module references resolve |
| `mcp_azure_mcp_deploy` | `deploy_pipeline_guidance_get` | Step 10 | CI/CD pipeline config. `is-azd-project: false`, `pipeline-platform: "github-actions"`, `deploy-option: "provision-and-deploy"` |
| `mcp_azure_mcp_deploy` | `deploy_plan_get` | Step 12 | Structured deployment plan as validation input. Set `source-type: "from-project"`, `target-app-service` from plan |
| `mcp_azure_mcp_azureterraformbestpractices` | *(flat)* | Step 5 | Terraform path only β patterns and conventions for HCL generation |
rbac-roles.md 3.8 KB
# RBAC β Common Roles Reference
Shared reference for role assignment GUIDs used in both Bicep and Terraform. Loaded by both pattern files.
## Deterministic Role Assignments
Use `guid()` (Bicep) or deterministic naming (Terraform) for reproducible assignment names. Always set `principalType` to `'ServicePrincipal'` β prevents AAD graph lookup delays.
If the required role is NOT in the table below, call `azure__documentation` with the target resource type to look up the correct built-in role definition ID before generating the role assignment.
| Role | ID | Use |
|------|-----|-----|
| AcrPull | `7f951dda-4ed3-4680-a7ca-43fe172d538d` | Container Apps β ACR |
| Key Vault Secrets User | `4633458b-17de-408a-b874-0445c86b69e6` | App β Key Vault secrets |
| Storage Blob Data Contributor | `ba92f5b4-2d11-453d-a403-e96b0029c9fe` | App β Storage blobs (read/write/delete) |
| Storage Blob Data Reader | `2a2b9908-6ea1-4ae2-8e65-a410df84e7d1` | App β Storage blobs (read-only) |
| Storage Queue Data Contributor | `974c5e8b-45b9-4653-ba55-5f855dd0fb88` | App β Storage queues |
| Storage Table Data Contributor | `0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3` | App β Storage tables |
| Azure Service Bus Data Sender | `69a216fc-b8fb-44d8-bc22-1f3c2cd27a39` | App β Service Bus (send) |
| Azure Service Bus Data Receiver | `4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0` | App β Service Bus (receive) |
| Azure Event Hubs Data Sender | `2b629674-e913-4c01-ae53-ef4638d8f975` | App β Event Hubs (send) |
| Azure Event Hubs Data Receiver | `a638d3c7-ab3a-418d-83e6-5f17a39d4fde` | App β Event Hubs (receive) |
| App Configuration Data Reader | `516239f1-63e1-4d78-a4de-a74fb236a071` | App β App Configuration (read) |
| SignalR Service Owner | `7e4f1700-ea5a-4f59-8f37-079cfe29dce3` | App β SignalR |
| Cognitive Services OpenAI User | `5e0bd9bd-7b93-4f28-af87-19fc36ad61bd` | App β Azure OpenAI (inference) |
| Search Index Data Reader | `1407120a-92aa-4202-b7e9-c0e197c71c8f` | App β AI Search (query) |
| Search Index Data Contributor | `8ebe5a00-799e-43f5-93ac-243d3dce84a7` | App β AI Search (read/write index) |
| Monitoring Metrics Publisher | `3913510d-42f4-4e42-8a64-420c390055eb` | App β Azure Monitor (custom metrics) |
## Cosmos DB β Data Plane RBAC (NOT ARM RBAC)
β **Cosmos DB uses its OWN role system** β do NOT use `Microsoft.Authorization/roleAssignments` for Cosmos DB data access. ARM RBAC roles (Contributor, Reader) grant control-plane access only. For data access (read/write documents), use `Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments`.
| System | Resource type | Use for |
|--------|--------------|--------|
| ARM RBAC | `Microsoft.Authorization/roleAssignments` | Subscription/RG-level permissions (Contributor, Reader, Key Vault Secrets User) |
| Cosmos DB data plane | `Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments` | Document read/write access (Data Reader, Data Contributor) |
Built-in Cosmos DB data roles:
- Data Reader: `00000000-0000-0000-0000-000000000001`
- Data Contributor: `00000000-0000-0000-0000-000000000002`
## Delegation
For complex RBAC scenarios (custom roles, cross-subscription, conditional access), AppOnboard does not generate custom role definitions inline. Instead, append an entry to `prepare-plan.json β postDeployRecommendations[]` (schema: `PostDeployRecommendation` in [`prepare-schemas.ts`](../../prepare/references/prepare-schemas.ts)):
```json
{
"title": "Configure custom RBAC role for <service>",
"reason": "<why the built-in roles above are insufficient>",
"effort": "medium",
"services": ["<affected-service>"]
}
```
At handoff, the agent will detect this recommendation and offer to call `mcp_azure_mcp_role` to list existing role assignments and create custom roles for the deployed resource group. See `handoff-protocol.md` Skill-Based Next Steps β the RBAC condition row triggers this automatically.
scaffold-healing-rules.md 3.3 KB
# Scaffold Healing Rules
Self-healing loop for scaffold validation failures. Contains error classification (FIXABLE vs BLOCKING) and scaffold-specific escalation and plan-change rules.
## Error Classification
| Error Type | Class | Auto-Fix Strategy |
|------------|-------|-------------------|
| Invalid property name | FIXABLE | Fix from schema |
| Syntax error (HCL/Bicep) | FIXABLE | Regenerate from references |
| Missing required property | FIXABLE | Call `mcp_bicep_build_bicep` for structured error, fix from diagnostics output |
| Wrong API version | FIXABLE | Call `mcp_bicep_list_az_resource_types_for_provider` with `{ providerNamespace: "..." }` for latest GA. Fallback: reference file examples |
| Provider version conflict | FIXABLE | Update `required_providers` |
| Undeclared variable | FIXABLE | Add to `variables.tf` |
| Policy-blocked SKU | FIXABLE | Next-best from `rejectedAlternatives[]` |
| Circular dependency | FIXABLE | Break cycle β refactor module refs |
| Permission/RBAC insufficient | BLOCKING | Surface role + `az role assignment create` |
| State backend inaccessible | BLOCKING | Surface `az storage account create` |
| Region unsupported | BLOCKING | Suggest alternate regions (user decision) |
| Quota exhaustion (all tiers+regions) | PLAN_LEVEL_CHANGE | β See Β§ PLAN_LEVEL_CHANGE |
| Quota exhaustion (single region) | PLAN_LEVEL_CHANGE | β Region pivot β skip `quotaValidation.checkedRegions` failures |
| Policy blocks service entirely | PLAN_LEVEL_CHANGE | β Map to `rejectedAlternatives[]` |
## Healing Escalation Cadence
Classify Step 11 failures using the table above. **FIXABLE:** edit IaC β re-validate (max 3 before asking). Each attempt must be a *different* fix. **BLOCKING:** surface to user, halt.
> β **After each FIXABLE auto-fix:** call `mcp_bicep_build_bicep` with `{ filePath: "infra/main.bicep" }` to quick-check before re-dispatching the validate sub-agent. If errors remain, fix again β saves a sub-agent dispatch. Fallback: `az bicep build`.
> β **After 3 attempts:** summarize error pattern, propose specific next fix (never "I'll try again" without naming the change), present options: **"Yes, try that"** (5 more) | **"I have a suggestion"** (apply user fix) | **"Stop"** (write `validationResult.status: "Failed"`). Ask again every 5 thereafter.
> β **Same error 3Γ:** read [iac-resources.md](../../references/iac-resources.md) for docs. Present to user; stop auto-healing that pattern.
## PLAN_LEVEL_CHANGE
> β Service type or region changes require user re-approval β never silently rewrite IaC.
| Step | Action |
|------|--------|
| 1. STOP | Never silently change service/region |
| 2. Update plan | `prepare-plan.json`: `services[]`, `naming.resources[]`, `costEstimate`, `deployStrategy` per [`prepare-schemas.ts`](../../prepare/references/prepare-schemas.ts) |
| 3. Re-approve | "β οΈ Plan change: {old} β {new} (~${new}/mo). Approve? (Yes / Edit / Cancel)" |
| 4. Regenerate | New module files with correct names, delete stale, re-run self-review + validation |
| 5. Log | [`ScaffoldHealingAttempt`](scaffold-schemas.ts) with `planLevelChange: true`. Counts toward healing cap |
> β **Artifact consistency:** After any service/region/SKU change, update ALL upstream artifacts (`prepare-plan.json`, `scaffold-manifest.json`, `context.json`, IaC files) β the plan MUST match deployed reality.
scaffold-schemas.ts 3.3 KB
/**
* Scaffold artifact schema β scaffold-manifest.json.
* Read by scaffold refs (validation-and-manifest.md, scaffold-healing-rules.md, iac-generation-rules.md).
*/
// βββ Scaffold healing types ββββββββββββββββββββββββββββββββββββββββββββββββββ
export type ScaffoldErrorClassification = "FIXABLE" | "BLOCKING";
export type ScaffoldHealingResult = "fixed" | "still-failing" | "blocked";
export interface ScaffoldHealingError {
check: string;
detail: string;
classification: ScaffoldErrorClassification;
}
export interface ScaffoldHealingFix {
file: string;
change: string;
reason: string;
}
export interface ScaffoldHealingAttempt {
attempt: number;
errors: ScaffoldHealingError[];
fixes: ScaffoldHealingFix[];
result: ScaffoldHealingResult;
/** True when this healing attempt changed the service type or region β requires re-approval */
planLevelChange?: boolean;
/** Original service before the pivot (e.g., "App Service B1") */
originalService?: string;
/** New service after the pivot (e.g., "Container Apps Consumption") */
newService?: string;
}
// βββ scaffold-manifest.json ββββββββββββββββββββββββββββββββββββββββββββββββββ
export type SelfReviewRating = "VERIFIED" | "PLAUSIBLE" | "FLAGGED";
export interface ScaffoldFile {
path: string;
type: string;
}
export interface SelfReviewFinding {
layer: string; // "L1" | "L2" | "L3" | "L4"
claim: string;
rating: SelfReviewRating;
detail: string;
}
export type IacFormat = "bicep" | "terraform";
export interface ValidationCheck {
name: string;
passed: boolean;
detail?: string;
}
export interface ValidationResult {
status: "Validated" | "Partial" | "Failed";
checks: ValidationCheck[];
proof?: string;
}
/** One BLOCK failure emitted by scaffold-conformance.{ps1,sh} (Step 3c plan-conformance gate). */
export interface ConformanceFailure {
id: string; // e.g. "TAGS-NO-CAMEL", "NO-PLAINTEXT-SECRET", "DB-TLS-ON"
detail: string;
file: string;
}
/** Result of the deterministic plan-conformance gate (subagent-validate.md Step 3c). */
export interface ConformanceResult {
passed: boolean;
failures: ConformanceFailure[];
/** "script" when scaffold-conformance.{ps1,sh} ran; "manual" when the fallback assertion table was used. */
source: "script" | "manual";
}
export interface ScaffoldManifest {
/** Session id this manifest belongs to. */
sessionId: string;
/** UTC timestamp when scaffold completed. */
scaffoldCompletedUtc: string;
iacFormat: IacFormat;
/** Deployment scope for the generated IaC (deploy preflight branches on this). */
targetScope: "subscription" | "resourceGroup";
files: ScaffoldFile[];
/** Entry-point IaC file, e.g. "infra/main.bicep". */
entryPoint?: string;
/** Parameters file, e.g. "infra/main.parameters.json". */
parametersFile?: string;
/** Exact command the deploy phase runs to provision (read by deploy preflight). */
deployCommand: string;
/** Container Apps two-phase (infra, then image) wiring, when applicable. */
twoPhaseWiring?: boolean;
/** Ordered phase-2 steps (e.g. image build/push) for two-phase deploys. */
phase2Steps?: readonly string[];
selfReview: {
findings: SelfReviewFinding[];
healingAttempts?: readonly ScaffoldHealingAttempt[];
};
validationResult?: ValidationResult;
conformance?: ConformanceResult;
}
self-healing.md 2.3 KB
# Self-Healing Loop β Error Classification & Auto-Fix
Step 11 runs validation against the generated IaC via CLI commands (`az bicep build` + `az deployment sub what-if`). On failure, classify each error and apply the strategy below. Max 3 attempts before pausing to present a diagnosis (explain pattern β propose fix β ask user). After user approves, 5 more attempts before asking again β then every 5 thereafter. See scaffold SKILL.md Β§ Self-Healing Loop for the full escalation protocol.
| Error Type | Class | Auto-Fix Strategy |
|------------|-------|-------------------|
| Invalid property name | FIXABLE | Replace with correct property from schema summary |
| Syntax error (HCL/Bicep) | FIXABLE | Re-generate affected module from reference patterns |
| Missing required property | FIXABLE | Add with default value from MCP best practices |
| Wrong API version | FIXABLE | Update to version from schema result |
| Provider version conflict | FIXABLE | Update `required_providers` block |
| Undeclared variable | FIXABLE | Add declaration to `variables.tf` |
| Policy-blocked SKU | FIXABLE | Substitute with next-best from `rejectedAlternatives[]` |
| Circular dependency | FIXABLE | Refactor module references β break cycle |
| Permission/RBAC insufficient | BLOCKING | Surface required role + `az role assignment create` command |
| State backend inaccessible | BLOCKING | Surface `az storage account create` instructions |
| Region unsupported for resource | BLOCKING | Suggest alternate regions β requires user decision |
| Quota exhaustion (ALL tiers in ALL regions) | PLAN_LEVEL_CHANGE | β Service type pivot required β see scaffold SKILL.md Β§ Self-Healing Loop. Update `prepare-plan.json` β present re-approval gate β regenerate IaC. Counts as 1 healing attempt |
| Quota exhaustion (single region) | PLAN_LEVEL_CHANGE | β Region pivot required β read `prepare-plan.json.quotaValidation.checkedRegions` and `failedResources` to skip already-failed regions. After checking new regions, append results back to these fields. See scaffold SKILL.md Β§ Self-Healing Loop. Update plan region β present re-approval β regenerate IaC |
| Policy blocks planned service entirely | PLAN_LEVEL_CHANGE | β Alternative service required β see scaffold SKILL.md Β§ Self-Healing Loop. Map to next-best from `rejectedAlternatives[]` β present re-approval β regenerate IaC |
self-review-checklist.md 8.9 KB
# Self-Review Checklist
4-layer adversarial review of generated IaC. Run after scaffold generates all files, before `scaffold-manifest.json`.
## Rating System
- **VERIFIED** β confirmed correct by inspecting the generated code
- **PLAUSIBLE** β likely correct but cannot fully verify (e.g., API version exists but not checked against registry)
- **FLAGGED** β incorrect, missing, or contradicts the plan/patterns
**Consume results:** If any finding is FLAGGED β fix the IaC, then re-run validation. If all VERIFIED/PLAUSIBLE β proceed. Write findings to `scaffold-manifest.json.selfReview`.
> β **Halt on critical failures** β if any finding is FLAGGED at L1 (Security) or L3 (Hallucination), do NOT proceed to deploy. Present findings and ask: **"Fix / Continue with risks / Cancel"**.
> β **Step 12 validation remains mandatory** regardless of self-review results β IaC may change during FLAGGED fixes.
## Layer 1 β Security Claims Extraction
Extract every security claim from the generated IaC and check for internal contradictions.
| Check | Example |
|-------|---------|
| Managed identity declared but secret hardcoded | `identity: { type: 'SystemAssigned' }` but `password: 'hardcoded'` in same file |
| HTTPS enforced but HTTP endpoint exposed | `httpsOnly: true` but ingress allows HTTP |
| Resource accessed via managed identity but matching role not granted | KV secret read without KV Secrets User, ACR image pull without AcrPull, or any MIβresource dependency missing its `roleAssignment` β access fails at runtime β `FLAGGED` |
| β Role assignment scope targets wrong resource | `scope: resourceGroup()` on resource-specific roles β `FLAGGED`. Must scope to specific resource. |
| `principalType` missing on role assignments | Causes intermittent 30s+ delays |
| β Identity block missing on compute resource | β **MANDATORY FAIL** β ALL compute MUST have `identity: { type: 'SystemAssigned' }`. β **HARD EXCEPTION β F1/D1 Linux:** MI sidecar causes OOM on free tier β rate `PLAUSIBLE`, **NEVER** `FLAGGED`. The gen template intentionally omits MI for F1/D1. If F1/D1 detected in plan, this check MUST be `PLAUSIBLE`. |
| SQL firewall `0.0.0.0/0` without private endpoint | Prefer MI + private endpoint. AllowAzureServices genuinely needed β `PLAUSIBLE`. |
| β SCM/FTP auth policy missing on App Service | ALL App Service MUST have `basicPublishingCredentialsPolicies`: `scm.allow: true`, `ftp.allow: false`. Missing β `FLAGGED`. |
| β KV URL uses `environment().suffixes.keyvaultDns` | Leading dot β double-dot URL β `ContainerAppSecretKeyVaultUrlInvalid`. Use `keyVault.name` + `.vault.azure.net` or `vaultUri` output. β β **FLAGGED** |
**Rating:** Claims that contradict each other β `FLAGGED`. Consistent claims β `VERIFIED`.
## Layer 2 β Pattern Validation
Validate against pattern files loaded at Steps 3β5 and [rbac-roles.md](rbac-roles.md).
### Bicep
| Check | Source |
|-------|--------|
| File structure: `main.bicep` β `modules/*.bicep` | `bicep-patterns.md` |
| `main.parameters.json` uses ARM JSON (not `.bicepparam`) | `bicep-patterns.md` |
| Naming: `{prefix}{name}{token}` β€32 chars | `bicep-patterns.md` |
| System-assigned managed identity on all services | `bicep-patterns-security.md` |
| No `administratorLogin` in generated Bicep | `bicep-patterns-security.md` |
| KV uses RBAC authorization (not access policies) | `bicep-patterns-security.md` |
| β `enablePurgeProtection` exists in KV module | Remove β `false` rejected by ARM, `true` blocks KV deletion β β **FLAGGED** |
| β KV deployer role assignment β `Key Vault Secrets Officer` for `deployerObjectId` scoped to KV resource | `bicep-patterns-security.md` Β§ Key Vault Deployer RBAC. Without this, `az keyvault secret set` fails with 403. |
| Prereq `warnings[]` each have a corresponding IaC fix | Read [`env-var-secrets.md`](env-var-secrets.md) for SSL/TLS fixes |
| Container Apps: two-phase ACR wiring, `registries` populated when ACR in plan, port alignment | `bicep-container-apps.md` |
| β BuildKit Dockerfile without `Dockerfile.azure` | `hasBuildKitSyntax == true` but no `Dockerfile.azure` in `files[]` β β **MANDATORY FAIL**. ACR does not support BuildKit. |
| β Role assignment `scope` targets specific resource, not `resourceGroup()` | `rbac-roles.md` |
| β No `azure.yaml` in `scaffold-manifest.json.files[]` | `pipeline-rules.md` |
| Non-Azure TF in separate dir | `terraform-patterns.md` |
### Cross-Module Reference Validation
Trace references BETWEEN modules β per-file checks miss broken cross-module wiring.
| Check | Rating |
|-------|--------|
| **Param wiring** β every `module` call in `main.bicep`: verify every param without `= default` is passed | Missing param β `FLAGGED` |
| **Secret ref completeness** β every CA `secrets[].keyVaultUrl` has a matching KV secret resource | Missing KV secret β `FLAGGED` |
| **Output ref validity** β every `moduleRef.outputs.X` is declared in the referenced module | Missing output β `FLAGGED` |
### Terraform
| Check | Source |
|-------|--------|
| File structure: `main.tf`, `variables.tf`, `outputs.tf`, `backend.tf`, `modules/` | `mcp_azure_mcp_azureterraformbestpractices` |
| Provider: `azurerm ~> 4.0` | Terraform registry |
| System-assigned managed identity, no `administrator_login`, KV RBAC | `terraform-patterns.md` |
| Container Apps: two-phase ACR wiring | Same pattern as Bicep |
**Rating:** Matches β `VERIFIED`. Reasonable deviation β `PLAUSIBLE`. Violates β `FLAGGED`.
## Layer 3 β Hallucination Detection
Catch fabricated resource types, API versions, SKU names, or properties.
### Bicep (default path)
| Check | How |
|-------|-----|
| API versions, resource types, property names valid | `bicep build` β errors = `FLAGGED` |
| β Deploy-time value validity (`bicep build` blind spot) | The compiler accepts any schema-valid string, but ARM rejects wrong enum-like values (engine versions, region-restricted SKUs), wrong resource `scope`, and empty resource-ID properties. Any such value the generator chose from memory β not traceable to `prepare-plan.json` or a pattern file β MUST be confirmed against the provider capabilities API; unconfirmed β `FLAGGED`. |
| SKU names match `prepare-plan.json` | Cross-reference `services[].sku` |
| OpenAI deployment uses `scaleSettings` instead of `sku` | `scaleSettings` deprecated β `FLAGGED` |
| Any resource uses `-preview` API version | Use latest GA from MCP tool β `FLAGGED` |
| VNet subnets as separate child resources | Must be inline in `properties.subnets[]` β `FLAGGED` |
Run `bicep build main.bicep --stdout > /dev/null` as syntax + schema validation. Parse errors = `FLAGGED`.
> β **`az bicep build` is MANDATORY for L3.** If unavailable, write `FLAGGED` with "bicep build unavailable."
> β **Verify `main.bicep` has `targetScope = 'subscription'`.** Missing β FLAGGED (FIXABLE β add targetScope, RG resource with tags, `scope: rg` on modules).
> `mcp_bicep_build_bicep` + `az deployment sub what-if` also appropriate at L3. β Do NOT use `az deployment sub validate` (known bug).
**Rating:** Passes `bicep build` β `VERIFIED`. Build warning β `PLAUSIBLE`. Build error β `FLAGGED`.
### Terraform (alternative path)
Run `terraform init -backend=false && terraform validate` + `terraform plan -detailed-exitcode`. Same rating criteria as Bicep.
**Rating:** Passes validate + plan β `VERIFIED`. Plan warning β `PLAUSIBLE`. Validate/plan error β `FLAGGED`.
## Layer 4 β WAF Alignment
Per-pillar spot check against [Azure Well-Architected Framework](https://learn.microsoft.com/en-us/azure/well-architected/).
| Pillar | Check |
|--------|-------|
| Reliability | Health probes configured. β Verify probe path matches `prereq-output.json.healthEndpoint` β non-existent or mismatched path = `FLAGGED`. β ACA probes do NOT follow HTTP redirects β if app has trailing-slash normalization (Express `redirect`), use path WITHOUT trailing `/` (e.g., `/app` not `/app/`). |
| Security | No public blob access, TLS 1.2+, managed identity |
| Cost | SKU matches budget tier from `prepare-plan.json` |
| Ops | App Insights present + connected for APM, 5 AppOnboard tags on resources, all values parameterized. Present = `VERIFIED`. App Insights absent = `PLAUSIBLE`. β `diagnostic-settings` is not part of the plan β if the generator added one it MUST be gated (`if (enableDiagnostics)`, default `false`) or absent; wired UNCONDITIONALLY in `main.bicep` = `FLAGGED` (blocks first deploy). |
| Performance | Autoscale rules present for production SKUs |
| Reliability | β Env var values compatible with app config validation (Pydantic `Settings`, Django `settings.py`). Typed fields reject wrong formats β `FLAGGED` |
**Rating:** Pillar addressed β `VERIFIED`. Not applicable for SKU β `PLAUSIBLE`. Missing for production SKU β `FLAGGED`.
## Output
Write findings to `scaffold-manifest.json.selfReview.findings[]`: `{ "layer": "L1", "claim": "...", "rating": "FLAGGED", "detail": "..." }` (layer is one of `"L1"`|`"L2"`|`"L3"`|`"L4"`). All `FLAGGED` must be resolved or surfaced at deploy gate. `PLAUSIBLE` = informational.
self-review-procedure.md 2.3 KB
# Self-Review Procedure β Step 9
Adversarial self-review using a sub-agent to perform L1βL4 review of generated IaC.
## Sub-Agent Setup
Use a sub-agent to perform the review. Provide:
- All generated IaC file contents (every .bicep or .tf file from Step 5)
- The `prepare-plan.json` services/naming/deploymentVariables sections
- The `scaffold-manifest.json.files[]` list
- The full content of [self-review-checklist.md](self-review-checklist.md) AND [waf-checklist.md](waf-checklist.md) verbatim
## Sub-Agent Prompt
> "Follow the self-review-checklist.md procedures for EACH of L1βL4. Rate each finding as VERIFIED | PLAUSIBLE | FLAGGED. Check: L1 Security (RBAC scope, network rules, managed identity, Key Vault β check contradictions between IaC and plan), L2 Pattern (anti-patterns, missing supporting resources β verify every file in scaffold-manifest.json.files[] exists on disk and is non-empty, FLAGGED if any missing or empty), L3 Hallucination (resource names match prepare-plan.json.naming exactly, API versions are real, SKU names match plan, no invented resource types), L4 WAF (use waf-checklist.md β Reliability, Security, Cost, Ops, Performance per-service checks). Do not fabricate results β check each claim against the actual IaC content provided. Return: { findings: [{ layer: 'L1'|'L2'|'L3'|'L4', claim: '...', rating: 'VERIFIED'|'PLAUSIBLE'|'FLAGGED', detail: '...' }], summary: 'N/N VERIFIED, N PLAUSIBLE, N FLAGGED' }. β€1000 tokens."
## Consume Results
- If any finding is FLAGGED β fix the IaC, then re-run validation (`az bicep build`, `az deployment sub what-if`)
- If all VERIFIED/PLAUSIBLE β proceed to Step 10
- Write findings to `scaffold-manifest.json.selfReview`
> β **Self-review is COMPLETE after L1βL4.** L3 may use `mcp_bicep_get_bicep_file_diagnostics`, `az bicep build`, or `az deployment sub what-if` β all are appropriate for catching errors early. **Step 12 remains mandatory regardless of what self-review found** β IaC may change between Steps 9β12 (FLAGGED fixes), and Step 12 writes the contractual `validationResult` to the manifest.
> β **Halt on critical self-review failures** β if any selfReview finding is FLAGGED at L1 (Security) or L3 (Hallucination), do NOT proceed to deploy. Present findings and ask: **"Fix / Continue with risks / Cancel"**.
subagent-iac-gen.md 9.4 KB
# Subagent Template β IaC Generation (Steps 5β8)
Generate deployment-ready IaC from `prepare-plan.json`. Follow the workflow below β each step specifies which reference to read and what to do with it.
## Critical Rules
- β **Do NOT invoke ANY skills** β no `{"skill": "azure-validate"}`, `{"skill": "azure-deploy"}`, `{"skill": "azure-prepare"}`, or any other skill call. Use the procedures in THIS file only.
- β **Do NOT generate `azure.yaml`**
- β **Do NOT modify app source code** β only write files under `infra/` (and `Dockerfile.azure` if needed)
- β **Do NOT run app build/test/lint commands** (`npm test`, `npm run build`, `pnpm build`, `python -m pytest`, `dotnet build`, etc.). Only validate generated IaC via `az bicep build`.
## Input (provided by caller)
| Field | Source | Required |
|-------|--------|----------|
| `prepare-plan.json` content | Session folder β services, naming, quotas, cost, deploymentVariables | YES (verbatim) |
| `context.json.overrides` | `iacFormat`, `detectedInfraProvider` | YES |
| `buildRequirements` | From `prereq-output.json` β runtime, deps, Dockerfiles | YES |
| `warnings[]` | From `prereq-output.json` β prereq warnings requiring IaC fixes (env var overrides, config changes). Applied during Steps 3β4. | YES |
| Compute targets | App Service/Functions, Container Apps, or both + whether PostgreSQL/Redis present | YES |
| `apiVersions` | Map of `resourceType β latestGAVersion` from main-thread MCP lookup. Use these versions in generated Bicep β do NOT use versions from training data. If `"MCP unavailable"` β see Step 1 for fallback. | YES |
## Output
| Artifact | Location |
|----------|----------|
| `infra/main.bicep` (or `main.tf`) | Workspace `infra/` |
| `infra/main.parameters.json` (or `variables.tf`) | Workspace `infra/` |
| `infra/modules/{service}.bicep` per service | Workspace `infra/modules/` |
| File list | Return to caller for `scaffold-manifest.json.files[]` |
## Workflow
### Step 1 β Read skeleton + tag patterns
Read [bicep-patterns.md](bicep-patterns.md) (Bicep) OR [terraform-patterns.md](terraform-patterns.md) (Terraform) β NOT both.
**Do:** Extract the `main.bicep` skeleton structure (targetScope, parameters, variables, resource group, module calls). Extract the 5-tag block definition. Use `prepare-plan.json.naming` for all resource names β never derive names with `take()`, `substring()`, `uniqueString()`, or string manipulation. The 4-char session suffix in the plan names already provides uniqueness. β For each `resource 'Type@Version'` declaration, use the version from `apiVersions` input. If type missing from map, use version from reference file examples.
> β **If `apiVersions` is `"MCP unavailable"` or missing a resource type:** run `az provider show --namespace {ns} --query "resourceTypes[?resourceType=='{type}'].apiVersions[?!contains(@, 'preview')] | [0][0]" -o tsv` for each missing provider β this filters to GA-only and picks the latest. NEVER fall back to training data β hallucinated API versions cause multiple deploy healing cycles.
### Step 2 β Read compute-target patterns
Read ONLY the compute-target reference(s) matching the plan, if the plan has multiple compute targets, read each matching file.:
- If plan has App Service/Functions β read [bicep-app-service.md](bicep-app-service.md).
- If plan has Container Apps β read [bicep-container-apps.md](bicep-container-apps.md).
- If plan has Static Web Apps β read [bicep-swa.md](bicep-swa.md).
- If plan has BOTH β read both.
**Do:** Generate compute module(s) using the patterns from each reference file. F1/D1 App Service: do NOT generate Dockerfile, do NOT add managed identity (OOM). App Service health probe: if `prereq-output.json.healthEndpoint` is non-null, set `siteConfig.healthCheckPath` to that value; otherwise omit (do NOT default to `/`).
### Step 3 β Read security patterns
β **You MUST read [bicep-patterns-security.md](bicep-patterns-security.md).** It contains Key Vault config, managed identity, HTTPS/TLS, and credential hygiene rules. Apply to every generated module.
### Step 4 β Read generation rules
Read [iac-generation-rules.md](iac-generation-rules.md).
**Do:** Apply ALL rules from the reference file to every generated module. The file contains mandatory tag definitions, naming constraints, security patterns, env var completeness checks, and Dockerfile generation rules. Do NOT skip any section β every rule applies.
### Step 5 β Read env var + secrets wiring
Read [env-var-secrets.md](env-var-secrets.md).
**Do:** For each component in the plan, read `.env.example` (or `.env.sample`, config files like Pydantic `Settings`, Django `settings.py`) from the workspace. Map every env var to either: (1) a Bicep parameter, (2) a KV secret reference, or (3) a value derived from other resources (e.g., DB connection string from the DB module output). Wire these into the compute module's `appSettings` (App Service) or `env` (Container Apps).
### Step 6 β Generate data modules (if needed)
ONLY if PostgreSQL, MySQL, or Redis is in the plan. Skip if none are present.
**PostgreSQL Flexible Server module** β include the `AllowAllAzureServicesAndResourcesWithinAzureIps` (`0.0.0.0`) firewall rule, extension allow-list (`azure.extensions` config: `uuid-ossp,pgcrypto,pg_trgm`), SSL enforcement, storage config (default 32 GB). Set the server `version` from `prepare-plan.json.services[].version` (capabilities-verified) β do NOT hardcode or guess. Use `@secure() param administratorLoginPassword` β deploy generates the value once and reuses it on redeploy; do NOT bake a value.
**MySQL Flexible Server module** β mirror the PostgreSQL module, with the MySQL-only deltas: `require_secure_transport: ON` config, the server `version` from `prepare-plan.json.services[].version` (ARM rejects major-only strings like `'8.0'` β needs an exact patch such as `'8.0.21'`), and a `flexibleServers/databases` child resource for the compose-declared DB name (e.g. `MYSQLDB_DATABASE`) so the app's schema DB exists in IaC before the container starts. See [bicep-patterns-data.md Β§ MySQL Flexible Server Module](bicep-patterns-data.md).
**Redis Cache module** β Basic SKU, `enableNonSslPort: false`, `minimumTlsVersion: '1.2'`. Store hostname + access key in Key Vault. β Known Bicep type issue: `sku` property may cause BCP035/BCP187 warnings β these are false positives. If deploy fails with `InvalidRequestBody` for `properties.sku.name`, create via `az redis create --sku Basic --vm-size c0` then reference with `existing` keyword in Bicep.
Wire connection strings via Key Vault `secretRef` (Container Apps) or `@Microsoft.KeyVault()` (App Service). β **Container Apps:** KV `secretRef` entries MUST be gated behind `isPlaceholder` β Phase 1 deploys with `secrets: []`. See [bicep-container-apps.md](bicep-container-apps.md) Β§ Two-Phase Wiring.
### Step 7 β Generate all files
> β **Before writing ANY file, verify:** (1) KV uses `enableRbacAuthorization: true`, NO `enablePurgeProtection`, NO access policies. (2) No secrets in module outputs β secrets flow through KV only. (3) API versions from `apiVersions` input, not memory. (4) Container Apps: no `revisionSuffix`, placeholder image as default, `isPlaceholder` conditionals on registries/secrets.
**Do:** Create the `infra/` directory and write all files:
1. `infra/bicepconfig.json` β write `{ "formatting": { "newlineKind": "LF" } }` if it doesn't already exist (user's repo may have one). LF is critical because Bicep triple-quoted strings pass content literally to ARM, and `\r` bytes crash `/bin/sh` in containers.
2. `infra/main.bicep` β subscription scope, RG creation with tags, module calls for all services + `role-assignments` module (KV deployer + app-to-KV RBAC), all unconditional.
3. `infra/main.parameters.json` β ARM JSON format (NOT `.bicepparam`). Include `environmentName`, `location`, `sessionId`, `deployedBy`, `createdAt`. β **`createdAt` value:** run `Get-Date -Format "o"` in terminal to get the current ISO 8601 timestamp β NEVER use a hardcoded or placeholder date. Do NOT include `@secure()` params (passed at deploy time). Include `deployerObjectId` param (deploy phase passes via `az ad signed-in-user show --query id -o tsv`).
4. `infra/modules/{service}.bicep` β one module per service from the plan, PLUS `role-assignments.bicep` (KV Secrets Officer for deployer, KV Secrets User for app identity if MI enabled β see [bicep-patterns-security.md](bicep-patterns-security.md) Β§ Key Vault Deployer RBAC).
5. If `buildRequirements.hasBuildKitSyntax == true`: β create `{component}/Dockerfile.azure` per [dockerfile-generation.md Β§ ACR Build Compatibility](dockerfile-generation.md).
6. If Container Apps and component has NO Dockerfile: read [dockerfile-generation.md](dockerfile-generation.md) and generate one. Follow the layer ordering, port alignment, and security defaults from that reference β do NOT generate from memory.
> β **Health probes for Container Apps:** Probe path priority: (1) `prereq-output.json.healthEndpoint` if non-null, (2) first detected GET route from the app, (3) `/` only if the app has a root handler. Do NOT default to `/` for APIs that only serve sub-paths β returns 404, blocks activation. For DB apps: use `/healthz` not `/readyz` (DB not wired in Phase 1).
### Step 8 β Validate syntax
**Do:** Run `az bicep build --file infra/main.bicep --stdout > $null`. If errors, fix and re-run (max 2 attempts). Do NOT use the `azure-validate` skill.
### Step 9 β Return results
**Do:** Return the list of generated files and any validation notes to the caller. Keep status report β€1500 tokens.
subagent-review.md 3.5 KB
# Subagent Template β Security + Adversarial Review (Steps 6β9)
Review generated IaC for security compliance and correctness. Follow the workflow below β each step specifies which reference to read and what to check.
## Critical Rules
- β **Do NOT invoke ANY skills** β no `{"skill": "azure-validate"}`, `{"skill": "azure-deploy"}`, `{"skill": "azure-prepare"}`, or any other skill call. Use the procedures in THIS file only.
- β **Do NOT run `az deployment` commands** β review is read-only analysis of generated files.
- β **Do NOT modify IaC files** β report findings only. The caller fixes issues.
## Input (provided by caller)
| Field | Required |
|-------|----------|
| All generated IaC file contents (every `.bicep` or `.tf` file) | YES |
| `prepare-plan.json` β services (service types, SKUs), naming, deploymentVariables sections | YES |
| `scaffold-manifest.json.files[]` list | YES |
| `prereq-output.json.warnings[]` β all prereq warnings that require IaC fixes | YES |
## Output
Return JSON (β€1000 tokens):
```json
{
"findings": [
{ "layer": "L1|L2|L3|L4", "file": "modules/app.bicep", "claim": "...", "rating": "VERIFIED|PLAUSIBLE|FLAGGED", "detail": "..." }
],
"summary": "N/N VERIFIED, N PLAUSIBLE, N FLAGGED"
}
```
## Workflow
### Step 1 β Read security patterns + run L1 security baseline
Read [bicep-patterns-security.md](bicep-patterns-security.md) and [rbac-roles.md](rbac-roles.md).
**Do:** Check every generated IaC file against ALL security checks defined in the reference file. The file contains the complete check table with FLAGGED conditions, edge cases, and Bicep code patterns. Do NOT guess checks from memory β use the reference file as the checklist.
### Step 2 β Read checklist + run L2βL4 adversarial review
Read [self-review-checklist.md](self-review-checklist.md).
**Do:** First run the **cross-module reference trace** from the checklist's Β§ Cross-Module Reference Validation: parse every `module` call in `main.bicep`, read each target module's `param`/`output` declarations and `secrets[]` entries, then verify every reference resolves (params passed match params declared, outputs referenced exist, every CA `secretRef` has a matching KV secret resource). Then run L2βL4:
- **L2 (Pattern Validation):** File structure matches `main.bicep` β `modules/*.bicep`, naming follows plan, Container Apps uses two-phase wiring, every `files[]` entry exists on disk, no `azure.yaml`, cross-module references all resolve
- **L3 (Hallucination Detection):** Resource names match `naming.resources[]` exactly, API versions are real (verify via `az bicep build`), SKU names match plan, no invented resource types
- **L4 (WAF Alignment):** Check per-pillar:
- Reliability: zone redundancy (prod SKUs), health probes, GRS storage, min replicas β₯1
- Security: managed identity, KV secrets, HTTPS+TLS 1.2, no public blob, no `administratorLogin`
- Cost: SKU matches budget, scale-to-zero for dev/test CA, free grants applied
- Ops: App Insights, 5 AppOnboard tags, all values parameterized
- Performance: autoscale (prod), CDN for SPA, connection pooling, cache tier
### Step 3 β Compile findings + return
**Do:** Merge L1βL4 results into the findings JSON. Apply rating per [self-review-checklist.md](self-review-checklist.md) Β§ Rating System: VERIFIED (evidence confirms claim), PLAUSIBLE (no counter-evidence but unverified), FLAGGED (evidence contradicts or missing critical pattern). β FLAGGED at L1 (Security) or L3 (Hallucination) β caller must fix before deploy. Return to caller.
subagent-validate.md 7.1 KB
# Subagent Template β Validation & Manifest (Steps 10β12)
Validate IaC syntax, write `scaffold-manifest.json`, and generate deploy checklist. Follow the workflow below.
## Critical Rules
- β **Do NOT invoke ANY skills** β no `{"skill": "azure-validate"}`, `{"skill": "azure-deploy"}`, `{"skill": "azure-prepare"}`, or any other skill call. Use the procedures in THIS file only.
- β **Do NOT create or modify Azure resources** β validation is syntax-only (`bicep build` / `terraform validate`), never `az deployment sub create`.
- β **Do NOT run `what-if` or `terraform plan`** β deploy runs the mandatory what-if with real secret params. Scaffold validates syntax only.
## Input (provided by caller)
| Field | Required |
|-------|----------|
| IaC file paths (all generated `.bicep` or `.tf` files) | YES |
| Self-review findings from Steps 6β9 | YES |
| `prepare-plan.json` β services, naming, region, subscriptionId | YES |
| `prereq-output.json.warnings[]` β prereq warnings with `fixPhase` | YES |
| `prereq-output.json.healthEndpoint` β detected health path (or `null`) | YES |
| Conformance result JSON (from main-thread Step 10a-conf) | YES |
## Output
| Artifact | Location |
|----------|----------|
| `scaffold-manifest.json` | Session folder |
| `deploy-result.json` skeleton | Session folder |
| Validation result status | Return to caller: `Validated` or `Failed` |
| Deploy checklist | `.copilot-azure/sessions/{id}/deploy-checklist.md` |
## Workflow
### Step 1 β Read validation + manifest rules
Read [validation-and-manifest.md](validation-and-manifest.md) and [scaffold-schemas.ts](scaffold-schemas.ts).
**Do:** Understand the `ScaffoldManifest` interface (field names, types, required fields) and the validation sequence.
### Step 2 β Format and validate IaC
**Do:**
1. Run `az bicep build --file infra/main.bicep --stdout > $null` (Bicep) or `terraform validate` (TF). Process output for BCP errors and warnings. Record pass/fail.
### Step 3 β Check RBAC completeness
**Do:** Verify every managed identity β resource pair in the IaC has a corresponding `Microsoft.Authorization/roleAssignments` resource with the correct role GUID. Cross-reference with the review findings from Steps 6β9 input.
### Step 3b β Azure runtime constraint check (Container Apps)
> Skip if no Container Apps in the plan.
For each Container App resource in the generated Bicep:
1. **cpu/memory combo** β verify `cpu` (must be type `string`) + `memory` is one of: `0.25/0.5Gi`, `0.5/1Gi`, `0.75/1.5Gi`, `1/2Gi`, `1.25/2.5Gi`, `1.5/3Gi`, `1.75/3.5Gi`, `2/4Gi`. FIXABLE: adjust to nearest valid combo (use smallest valid combo for sidecars/companions).
2. **secretRef coverage** β every `secretRef` in container env vars must have a matching entry in `configuration.secrets[]`. Every KV secret URL in `secrets[]` must reference a `Microsoft.KeyVault/vaults/secrets` resource that exists in the generated modules. Missing secret resource β FIXABLE: add it to the KV module.
3. **probe path** β if `prereq-output.json.healthEndpoint` is non-null, verify `probePath` matches it. If null AND any `plainEnvVars` entry is named `BASE` or `PATH_PREFIX`, verify `probePath` starts with that value (not bare `/`). If null AND no BASE var: verify the app has a route handler for the probe path (check source entry point for `app.get('/')` or framework root handler) β bare `/` on a REST API with only sub-path routes (e.g., `/users`, `/messages`) returns 404 and blocks revision activation. FIXABLE: update `probePath` to a known GET endpoint from the app.
FIXABLE errors: fix the Bicep β re-run `az bicep build` β proceed to Step 3c.
### Step 3b2 β App Service security constraint check
> Skip if no App Service or Functions in the plan.
For each App Service / Functions resource in the generated Bicep:
1. **basicPublishingCredentialsPolicies** β verify both child resources exist: `basicPublishingCredentialsPolicies/scm` (with `allow: true`) and `basicPublishingCredentialsPolicies/ftp` (with `allow: false`). Missing β FIXABLE: add the child resources per [bicep-patterns-security.md](bicep-patterns-security.md) Β§ Publishing Credential Lockdown.
2. **uniqueString in naming** β verify the App Service name uses `uniqueString()` or a unique suffix (not a hardcoded literal). Hardcoded names cause global collisions. Missing β FIXABLE: wrap name with `uniqueString(resourceGroup().id)`.
FIXABLE errors: fix the Bicep β re-run `az bicep build` β proceed to Step 3c.
### Step 3c β Record plan conformance result
The main thread (SKILL.md Step 10a-conf) already ran the conformance script and passed you its JSON. Record it in `scaffold-manifest.json.conformance` = `{ passed, failures, source: "script" }`. β Do NOT set `validationResult.status: "Validated"` while any BLOCK failure is unresolved.
**Fallback** (only if the caller passed NO result AND `infra/main.bicep` exists β the gate is Bicep-only, skip for Terraform): run `{scaffoldDir}/scripts/scaffold-conformance.ps1 -SessionPath "{sessionPath}" -InfraPath infra` (or `.sh` on bash) yourself, then record with `source: "script"`. Never hand-judge when a shell is available.
### Step 4 β Write scaffold-manifest.json
Read [scaffold-schemas.ts](scaffold-schemas.ts) for exact field names.
**Do:** Write `scaffold-manifest.json` to the session folder with: `sessionId`, `scaffoldCompletedUtc`, `iacFormat`, `targetScope`, `entryPoint`, `parametersFile`, `files[]`, `deployCommand`, `twoPhaseWiring` (if Container Apps), `phase2Steps` (if applicable), `selfReview` (from caller input), `validationResult` (from Steps 2β3). Use the exact field names from [scaffold-schemas.ts](scaffold-schemas.ts) Β§ `ScaffoldManifest`.
### Step 5 β Handle failures (if any)
Read [scaffold-healing-rules.md](scaffold-healing-rules.md) ONLY if validation failed.
**Do:** Classify errors as FIXABLE or BLOCKING. FIXABLE: auto-fix IaC β re-validate (max 3 attempts before asking user). BLOCKING: surface to user and halt. PLAN_LEVEL_CHANGE: requires re-approval β do NOT auto-fix.
### Step 6 β Verify deploy checklist exists
**Do:** Check that `.copilot-azure/sessions/{id}/deploy-checklist.md` exists (written by the parallel checklist subagent at scaffold Step 5b). If missing, read [`deploy-checklist-template.md`](../../deploy/references/deploy-checklist-template.md) and write it now as a fallback β fill `{placeholders}` from `prepare-plan.json`, delete non-applicable sections. This file survives conversation compaction
### Step 7 β Create deploy-result.json skeleton (MANDATORY)
Read [`deploy-schemas.ts`](../../deploy/references/deploy-schemas.ts) β specifically the `DeployResult` interface.
**Do:** Create `.copilot-azure/sessions/{id}/deploy-result.json` conforming to the `DeployResult` interface. Populate fields from all session artifacts already written (`prepare-plan.json`, `context.json`, `scaffold-manifest.json`, `prereq-output.json`). Use sensible defaults for fields the deploy phase will fill later. The deploy main agent updates this file in-place at Step 8 with real values.
### Step 8 β Return results
**Do:** Return validation status (`Validated` or `Failed`) to the caller. Confirm deploy-checklist.md exists. Keep status report β€500 tokens.
terraform-patterns.md 6.7 KB
# Terraform Patterns
Alternative-path patterns for AppOnboard scaffold. Used when `.tf` files detected or user overrides `iacFormat`. Per-resource config comes from `mcp_azure_mcp_azureterraformbestpractices` at runtime β this file covers layout, provider, naming, state, tagging, and wiring.
## File Structure
### Default (greenfield or user override)
```
infra/
βββ main.tf # Root module β provider, resource group, module calls
βββ variables.tf # Input variables (all configurable values)
βββ outputs.tf # Exported values (endpoints, resource IDs)
βββ backend.tf # State backend config (local default, Azure Storage for prod)
βββ terraform.tfvars # Default variable values (from prepare-plan.json)
βββ modules/
βββ app-service/
β βββ main.tf
β βββ variables.tf
β βββ outputs.tf
βββ container-app/
βββ sql-database/
βββ key-vault/
βββ log-analytics/
βββ ...
```
### Non-Azure IaC coexistence (GCP/AWS TF already in repo)
When `detectedInfraProvider.terraform` is `"gcp"`, `"aws"`, or `"multi"` (without `azurerm`), write Azure TF to a **separate directory** from existing non-Azure TF. Never overwrite or modify existing IaC files.
**Output directory rule:** If existing TF is NOT at `infra/`, write to `infra/`. If existing TF IS at `infra/` (or any path containing `infra`), write to `infra-azure/`. Same module structure as default layout.
Each Azure service gets its own module. `main.tf` orchestrates resource group + module calls.
## Provider Configuration
```hcl
terraform {
required_version = ">= 1.5"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
}
}
provider "azurerm" {
features {}
subscription_id = var.subscription_id
resource_provider_registrations = "none"
}
```
> β Never pin to exact patch versions (e.g., `= 4.1.0`). Use `~> 4.0` to allow minor/patch updates. `azurerm` manages API versions internally β if a resource isn't available in `azurerm`, use `azapi_resource` with the latest stable ARM API version.
> **Conditional access (AADSTS530084):** `azurerm` provider re-requests tokens that violate device-binding policies. Fix: (1) switch to Bicep, or (2) use service principal auth (`ARM_CLIENT_ID` + `ARM_CLIENT_SECRET` + `ARM_TENANT_ID`).
## variables.tf
Required variables: `environment_name` (string, default "dev"), `location` (string, default "eastus"), `subscription_id` (string), `session_id` (string), `deployed_by` (string). All configurable values MUST be variables β no hardcoded regions, names, or SKUs.
## terraform.tfvars
Populate from `prepare-plan.json`: `environment_name`, `location`, `subscription_id`, `session_id`. See naming-patterns.md for naming convention.
## Backend
Local backend by default: `backend "local" { path = "terraform.tfstate" }`. Recommend Azure Storage backend in `postDeployRecommendations[]` for production.
## Resource Group
Use `rg-${var.project_name}-${var.environment_name}-${random_string.suffix.result}` with `tags = local.tags`. Suffix prevents collisions across AppOnboard sessions.
## Naming Convention
```hcl
resource "random_string" "suffix" {
length = 4
special = false
upper = false
}
locals {
# Pattern: {type}-{appname}-{env}-{suffix}
app_name = "app-${var.environment_name}-${random_string.suffix.result}"
kv_name = "kv-${var.environment_name}-${random_string.suffix.result}"
sql_name = "sql-${var.environment_name}-${random_string.suffix.result}"
# Storage/ACR: alphanumeric only, no hyphens
storage_name = "st${replace(var.environment_name, "-", "")}${random_string.suffix.result}"
acr_name = "cr${replace(var.environment_name, "-", "")}${random_string.suffix.result}"
}
```
Cross-reference naming with [prepare/references/naming-patterns.md](../../prepare/references/naming-patterns.md) β Terraform names must match `prepare-plan.json.naming.resources[]`.
## Resource Tags β Mandatory
Apply all 5 AppOnboard tags via `local.tags` β see [iac-generation-rules.md Β§ Session Tags](iac-generation-rules.md) for tag names and values.
```hcl
locals {
tags = {
"app-onboard-skill" = "true"
"app-onboard-session-id" = var.session_id
"created-at" = timestamp()
"environment" = var.environment_name
"deployed-by" = var.deployed_by
}
}
```
> β οΈ `timestamp()` changes on every plan. Add `lifecycle { ignore_changes = [tags["created-at"]] }` on every resource.
## Secrets β random_password, Not random_string
```hcl
resource "random_password" "db_password" {
length = 32
special = true
lifecycle { ignore_changes = [result] }
}
resource "azurerm_key_vault_secret" "db_password" {
name = "db-password"
value = random_password.db_password.result
key_vault_id = azurerm_key_vault.kv.id
}
```
> β NEVER use `random_string` for secrets β it is not marked `sensitive` in state. Always use `random_password`.
## Container Apps β Two-Phase Wiring
Same circular dependency as Bicep β see [bicep-container-apps.md](bicep-container-apps.md). Phase 1: placeholder image, no ACR/KV refs. Phase 2: build + push, assign AcrPull, update via `az containerapp update --image` outside Terraform.
```hcl
# Phase 1: Placeholder image
resource "azurerm_container_app" "app" {
template {
container {
name = "app"
image = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest"
cpu = 0.25
memory = "0.5Gi"
}
}
identity {
type = "SystemAssigned"
}
}
```
## outputs.tf
Export: `resource_group_name`, `app_url` (https://${hostname}), `resource_ids` (list of all deployed resource IDs for deploy-result.json).
## Security Defaults
Apply same security rules as Bicep β see [bicep-patterns-security.md](bicep-patterns-security.md). Terraform-specific syntax:
| Rule | Terraform HCL |
|------|---------------|
| Managed identity | `identity { type = "SystemAssigned" }` |
| β No SQL admin password | `azuread_authentication_only = true`. Never generate `administrator_login_password` |
| Key Vault RBAC | `enable_rbac_authorization = true` on `azurerm_key_vault` |
| KV secret reference | `app_settings = { KEY = "@Microsoft.KeyVault(VaultName=..;SecretName=..)" }` |
| HTTPS only | `https_only = true`, `minimum_tls_version = "1.2"` |
| Storage | `https_traffic_only_enabled = true`, `allow_nested_items_to_be_public = false`, `min_tls_version = "TLS1_2"` |
| β Cosmos DB RBAC | `azurerm_cosmosdb_sql_role_assignment`, NOT `azurerm_role_assignment` β see [rbac-roles.md](rbac-roles.md) |
| RBAC assignments | `principal_type = "ServicePrincipal"` REQUIRED β see [rbac-roles.md](rbac-roles.md) |
| SCM/FTP auth | `scm.allow: true` (scaffold), `ftp.allow: false` (always) β use `azapi_resource` |
validation-and-manifest.md 5.1 KB
# Validation, Manifest & Approval β Steps 10β12.5
## Step 10 β CI/CD
Do NOT auto-generate workflow files or create branches/PRs. Scaffold only writes IaC files. This step activates only when `context.json.repo.remote` is non-null AND user explicitly requests branch/PR creation. When `repo.remote` is absent or user declines, write IaC directly to working tree. If the user asks for CI/CD, or after deploy completes, suggest it as a follow-up: call `mcp_azure_mcp_deploy` β `deploy_pipeline_guidance_get` with `is-azd-project: false`, `pipeline-platform: "github-actions"`, `deploy-option: "provision-and-deploy"` and present the guidance for the user to apply.
## Step 11 β Validate Generated IaC
> β **Validation MUST happen BEFORE the manifest is written.** The manifest requires `validationResult` β you cannot write it without completing validation first. Do NOT write `scaffold-manifest.json` until validation has run.
> β Do NOT call other skills during scaffold/deploy β see [pipeline-rules.md](../../references/pipeline-rules.md).
Run these checks directly. All must pass.
**11a. Bicep compilation:**
```powershell
az bicep build --file infra/main.bicep --stdout > $null
```
(Bash: redirect to `/dev/null` instead of `$null`.) Pass: exit 0. Fail: fix errors and retry.
**11b. Static RBAC review** β review generated Bicep for correct role assignments per [rbac-roles.md](rbac-roles.md). Every managed identity β resource pair must have a `Microsoft.Authorization/roleAssignments` resource with the correct role GUID.
**11c. Write `validationResult`** β after all checks pass, write:
```json
{
"validationResult": {
"status": "Validated",
"checks": [
{ "name": "bicep build", "result": "PASS" },
{ "name": "RBAC review", "result": "PASS" }
]
}
}
```
**Terraform path:** Replace 11a with `terraform init -backend=false && terraform validate`.
- If validation finds FIXABLE errors: edit IaC β re-run self-review (L1βL4) β re-validate (max 3 attempts). β **You MUST read [scaffold-healing-rules.md](scaffold-healing-rules.md) before entering the healing loop** β it defines error classification (FIXABLE vs BLOCKING) and auto-fix strategies.
- If BLOCKING errors remain after 3 attempts: surface to user and halt.
## Step 12 β Write `scaffold-manifest.json`
β **You MUST read [`scaffold-schemas.ts`](scaffold-schemas.ts)** to get the exact `ScaffoldManifest` interface. Write to the session folder with ALL fields populated: `files[]`, `selfReview.findings[]`, AND `validationResult` (from Step 11). This is a single write β validation is already complete.
> β **Phase exit gate: `scaffold-manifest.json.validationResult` MUST NOT be null.** If validation ran: `{ status: 'Validated'/'Partial'/'Failed', details }` (per `ValidationResult` in [`scaffold-schemas.ts`](scaffold-schemas.ts)). Null = incomplete scaffold.
**You MUST also update `context.json`** per `AppOnboardContext` in [`session-schemas.ts`](../../references/session-schemas.ts): append `"scaffold"` to `completedPhases`, set `currentPhase` to `"deploy"`, update `lastModifiedUtc`.
## Step 12.5 β Deploy Approval Gate
Present the user with: files generated, selfReview findings, **validation results** (pass/fail per check from Step 11), services + SKUs, secure-defaults applied. End with: **"Ready to deploy? (Yes / Run manually / Edit plan / Cancel)"** β do not continue until the user approves.
> β **Self-check before presenting the deploy gate.** Does `scaffold-manifest.json` contain a `validationResult` field with `status` set? If NO β you skipped Step 11. Go back and run validation. Do NOT present the deploy gate with `validationResult: null`.
> β **Quota gate β MANDATORY.** Read `prepare-plan.json.quotaValidation`. If `verified == false`, `method == "unverifiable"`, or `method` is not `"cli"` for quota-constrained services: read [`sku-quota-validation.md`](../../prepare/references/sku-quota-validation.md) Β§ Deploy Gate Re-Validation and follow the procedure.
> β Do NOT use `az vm list-usage`, `az appservice list-locations`, or `mcp_azure_mcp_quota` for quota checks β see Anti-Patterns in [`sku-quota-validation.md`](../../prepare/references/sku-quota-validation.md).
> β **Azure service compatibility warnings.** Read `prereq-output.json.warnings[]` for any warnings with `fixPhase: "deploy-gate"`. Surface EACH at the deploy gate: "β οΈ Azure compatibility: {warning.summary}. Fix: {warning.fix}. Approve? (Yes / Skip / Cancel)". If the user skips, add to `postDeployRecommendations[]`.
> β **Phase exit β NOT complete until ALL done:**
> 1. `scaffold-manifest.json` written with `files[]`, `selfReview.findings[]`, AND `validationResult`
> 2. `context.json`: `"scaffold"` appended to `completedPhases`, `currentPhase` β `"deploy"`, `lastModifiedUtc` updated
> 3. `deploy-checklist.md` exists in session folder β written by the parallel checklist subagent at scaffold Step 5b (before IaC gen completes). Verify it exists. If missing (subagent failed), write it now from [`deploy-checklist-template.md`](../../deploy/references/deploy-checklist-template.md) β fill in real values from `prepare-plan.json`, delete unrelated compute sections.
waf-checklist.md 2.3 KB
# WAF Checklist
Per-pillar Well-Architected Framework alignment for AppOnboard-generated infrastructure. Use during scaffold self-review (Layer 4) and prepare validation (WAF Alignment dimension).
> **Reference:** [Azure Well-Architected Framework](https://learn.microsoft.com/en-us/azure/well-architected/) β link, don't duplicate. See [WAF Service Guides](https://learn.microsoft.com/en-us/azure/well-architected/service-guides/) for per-service checklists.
## Reliability
- Zone redundancy enabled for production SKUs (App Service P1v3+, SQL Premium, Redis Premium)
- Health probes configured (Container Apps liveness/readiness, App Service `/health`)
- GRS storage for production data (Standard_GRS or RA-GRS)
- Retry policies in application code for transient failures
- Min replicas β₯ 1 for production Container Apps (no cold-start)
## Security
- System-assigned managed identity on all services
- Key Vault for all secrets (no inline connection strings)
- HTTPS-only + TLS 1.2+ on all endpoints
- No public blob access on storage accounts
- Private endpoints where budget allows (balanced/performance tiers)
- No `administratorLogin` for SQL (Entra-only auth)
## Cost Optimization
- SKU matches `prepare-plan.json` budget tier β no over-provisioning
- Scale-to-zero enabled for dev/test Container Apps
- Free tier grants applied in cost estimate (see [pricing-guide.md](../../prepare/references/pricing-guide.md) Β§ Free Grants Summary)
- Reserved instances noted as option for production (don't auto-apply)
## Operational Excellence
- `diagnostic-settings` is not scaffolded; if the generator added one it MUST be gated behind `enableDiagnostics` (default `false`) or absent β never wired unconditionally (that blocks the first deploy).
- Application Insights connected for APM
- Resource tagging: `app-onboard-skill`, `app-onboard-session-id`, `created-at` (see `bicep-patterns.md` Β§ Service Tagging or `terraform-patterns.md` Β§ Resource Tags)
- All configurable values parameterized (no hardcoded regions, names, SKUs)
## Performance Efficiency
- Autoscale rules for production SKUs (Container Apps max replicas, App Service auto-scale)
- CDN for static assets when SPA frontend detected
- Connection pooling for database access
- Appropriate cache tier (Redis) when session/cache pattern detected
scaffold-conformance.ps1 12.8 KB
#!/usr/bin/env pwsh
# Scaffold -> Deploy conformance gate.
# Checks generated IaC against the plan for the semantic defects `az bicep build`
# cannot catch (valid Bicep, wrong values) β the class that causes deploy healing.
#
# Usage: scaffold-conformance.ps1 -SessionPath <.copilot-azure/sessions/{id}> -InfraPath <infra>
# Output: JSON { passed, failures:[{id,detail,file}] } to stdout.
# Exit: 0 = pass, 1 = one or more BLOCK failures.
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string]$SessionPath,
[string]$InfraPath = 'infra'
)
$failures = New-Object System.Collections.Generic.List[object]
function Add-Fail($id, $detail, $file) {
$failures.Add([ordered]@{ id = $id; detail = $detail; file = $file })
}
function Read-Json($p) {
if (Test-Path $p) { try { return (Get-Content $p -Raw | ConvertFrom-Json) } catch { return $null } }
return $null
}
# --- Load artifacts (a missing input skips only the checks that need it) ---
$plan = Read-Json (Join-Path $SessionPath 'prepare-plan.json')
$mainPath = Join-Path $InfraPath 'main.bicep'
$paramsPath = Join-Path $InfraPath 'main.parameters.json'
$mainBicep = if (Test-Path $mainPath) { Get-Content $mainPath -Raw } else { '' }
$paramsRaw = if (Test-Path $paramsPath) { Get-Content $paramsPath -Raw } else { '' }
$iac = ''
if (Test-Path $InfraPath) {
$iac = (Get-ChildItem $InfraPath -Recurse -Filter *.bicep -ErrorAction SilentlyContinue |
ForEach-Object { Get-Content $_.FullName -Raw }) -join "`n"
}
$svcNames = @()
if ($plan -and $plan.services) { $svcNames = @($plan.services | ForEach-Object { $_.name }) }
function Has-Service($re) { return [bool]($svcNames | Where-Object { $_ -match $re }) }
# 1. TAGS-NO-CAMEL β tag keys must be hyphenated, not Bicep camelCase identifiers.
if ($mainBicep -match 'appOnboard(Skill|SessionId)|createdAt:|deployedBy:') {
Add-Fail 'TAGS-NO-CAMEL' "camelCase tag keys found β use hyphenated ('app-onboard-skill' ...)" 'infra/main.bicep'
}
# 12. DIAG-GATED β we don't scaffold diagnostic-settings; if the generator added one it must be gated behind
# enableDiagnostics (default false) or removed. Unconditional wiring blocks the first deploy (target/workspace not ready).
# Match an actual diagnostic MODULE/RESOURCE declaration, not a prose comment mentioning 'diagnostic'.
if ($mainBicep -match 'module\s+\w*[Dd]iagnostic|Microsoft\.Insights/diagnosticSettings' -and $mainBicep -notmatch 'enableDiagnostics') {
Add-Fail 'DIAG-GATED' 'diagnostic-settings module wired without an enableDiagnostics gate β gate it (module ... = if (enableDiagnostics), default false) or remove it; unconditional wiring blocks the first deploy' 'infra/main.bicep'
}
# 2. NO-PLAINTEXT-SECRET β secrets are @secure() params passed at deploy, never literals.
if ($paramsRaw) {
try {
$pj = $paramsRaw | ConvertFrom-Json
foreach ($k in $pj.parameters.PSObject.Properties.Name) {
if ($k -match '(?i)password|secret|connstring|connection') {
$val = $pj.parameters.$k.value
if ($null -ne $val -and "$val".Trim().Length -gt 0) {
Add-Fail 'NO-PLAINTEXT-SECRET' "parameter '$k' has a literal value β must be @secure(), passed at deploy" 'infra/main.parameters.json'
}
}
}
} catch { }
}
# 2b. NO-BICEP-LITERAL-SECRET β secret values must be @secure() params, never quoted literals in Bicep.
# Property name must end in 'password'/'connectionString' right before ':' (so 'secretName' etc. never match).
if ($iac) {
foreach ($m in [regex]::Matches($iac, "(?im)\b([A-Za-z]*(?:password|connectionstring))\s*:\s*'([^']+)'")) {
Add-Fail 'NO-BICEP-LITERAL-SECRET' "property '$($m.Groups[1].Value)' assigned a literal secret in Bicep β use an @secure() param passed at deploy" 'infra/'
}
if ($iac -match "(?im):\s*'[^']*(?:Password=|AccountKey=|SharedAccessKey=|://[^:@/\s]+:[^:@/\s]+@)[^']*'") {
Add-Fail 'NO-BICEP-LITERAL-SECRET' 'quoted literal contains an embedded credential (Password=/AccountKey=/user:pass@) in Bicep β use an @secure() param' 'infra/'
}
}
# 3. SERVICES-COMPLETE β every planned service maps to a resource type present in the IaC.
$typeMap = [ordered]@{
'container apps environment' = 'Microsoft\.App/managedEnvironments'
'container app' = 'Microsoft\.App/containerApps'
'container registry' = 'Microsoft\.ContainerRegistry/registries'
'mysql' = 'Microsoft\.DBforMySQL/flexibleServers'
'postgres' = 'Microsoft\.DBforPostgreSQL/flexibleServers'
'key vault' = 'Microsoft\.KeyVault/vaults'
'log analytics' = 'Microsoft\.OperationalInsights/workspaces'
'application insights' = 'Microsoft\.Insights/components'
'static web app' = 'Microsoft\.Web/staticSites'
'app service' = 'Microsoft\.Web/sites'
'functions' = 'Microsoft\.Web/sites'
'sql' = 'Microsoft\.Sql/servers'
'cosmos' = 'Microsoft\.DocumentDB/databaseAccounts'
'redis' = 'Microsoft\.Cache/redis'
'storage' = 'Microsoft\.Storage/storageAccounts'
'service bus' = 'Microsoft\.ServiceBus/namespaces'
'event hub' = 'Microsoft\.EventHub/namespaces'
}
if ($iac) {
foreach ($name in $svcNames) {
$lc = "$name".ToLower(); $expected = $null
foreach ($k in $typeMap.Keys) { if ($lc.Contains($k)) { $expected = $typeMap[$k]; break } }
if ($expected -and ($iac -notmatch $expected)) {
Add-Fail 'SERVICES-COMPLETE' "planned service '$name' has no matching resource ($expected) in IaC" 'infra/'
}
}
}
# --- Managed database checks ---
$hasMysql = Has-Service 'MySQL'
$hasPg = Has-Service 'PostgreSQL|Postgres'
if (($hasMysql -or $hasPg) -and $iac) {
# 4. DB-VERSION-MATCH β each server's version must equal its capabilities-verified plan value.
# Scope the match to the DB resource block so an unrelated 'version:' elsewhere isn't picked up.
$dbTypeRe = @{ 'MySQL' = 'Microsoft\.DBforMySQL/flexibleServers'; 'PostgreSQL' = 'Microsoft\.DBforPostgreSQL/flexibleServers' }
foreach ($svc in ($plan.services | Where-Object { $_.name -match 'MySQL|PostgreSQL' -and $_.version })) {
$kind = if ($svc.name -match 'MySQL') { 'MySQL' } else { 'PostgreSQL' }
$m = [regex]::Match($iac, "$($dbTypeRe[$kind])[\s\S]{0,800}?version:\s*'([^']+)'")
if ($m.Success -and $m.Groups[1].Value -ne $svc.version) {
Add-Fail 'DB-VERSION-MATCH' "$kind IaC version '$($m.Groups[1].Value)' != plan '$($svc.version)'" 'infra/modules'
}
}
# 5. DB-TLS-ON (MySQL) β require_secure_transport must be enforced.
if ($hasMysql -and ($iac -notmatch 'require_secure_transport')) {
Add-Fail 'DB-TLS-ON' 'MySQL module missing require_secure_transport config' 'infra/modules'
}
# 6. DB-NAME-PRESENT β the app's named DB must exist in IaC (from prepare-plan.appDbName).
if ($plan.appDbName -and ($iac -notmatch 'flexibleServers/databases')) {
Add-Fail 'DB-NAME-PRESENT' "app DB '$($plan.appDbName)' declared but no flexibleServers/databases resource" 'infra/modules'
}
# 10. MYSQL-NO-NETWORK-BLOCK (MySQL) β the public-access flow must OMIT the network block.
# An empty delegatedSubnetResourceId/privateDnsZoneResourceId is rejected by ARM (LinkedInvalidPropertyId).
if ($hasMysql -and ($iac -match 'delegatedSubnetResourceId|privateDnsZoneResourceId')) {
Add-Fail 'MYSQL-NO-NETWORK-BLOCK' 'MySQL module includes a network block (delegatedSubnetResourceId) β omit it for public access; an empty value is rejected by ARM' 'infra/modules'
}
# 11. DB-LOGIN-NOT-RESERVED β administratorLogin must not be an Azure-reserved name.
if ($iac -match "administratorLogin:\s*'(root|admin|administrator|guest|public|sa|azure_superuser|azure_pg_admin)'") {
Add-Fail 'DB-LOGIN-NOT-RESERVED' "administratorLogin uses a reserved name ('$($Matches[1])') β derive a safe login (e.g. '{project}admin'), never a compose-sourced reserved name" 'infra/modules'
}
}
# --- Key Vault checks ---
# Gate on the IaC resource (matches the .sh twin and this script's own CA/ACR gating) β NOT the plan
# service name, which misses KV when the plan names the service anything other than 'Key Vault'.
if (($iac -match 'Microsoft\.KeyVault/vaults')) {
# 7. KV-NO-PURGE β subscription policy rejects enablePurgeProtection:false; omit it entirely.
if ($iac -match 'enablePurgeProtection') {
Add-Fail 'KV-NO-PURGE' 'enablePurgeProtection present β omit it (policy may reject false)' 'infra/modules'
}
# 8. KV-DEPLOYER-ROLE β deployer needs Key Vault Secrets Officer + a deployerObjectId param.
$hasOfficer = $iac -match 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7'
$hasParam = ($paramsRaw -match 'deployerObjectId') -or ($iac -match 'deployerObjectId')
if (-not ($hasOfficer -and $hasParam)) {
Add-Fail 'KV-DEPLOYER-ROLE' 'missing deployer Key Vault Secrets Officer role and/or deployerObjectId param' 'infra/modules/role-assignments.bicep'
}
}
# --- Container Apps / ACR checks (deterministic ARM-failure invariants) ---
$hasCA = $iac -match 'Microsoft\.App/containerApps'
$hasCAE = $iac -match 'Microsoft\.App/managedEnvironments'
$hasAcr = $iac -match 'Microsoft\.ContainerRegistry/registries'
if ($iac) {
# 13. CAE-APPLOGS β managedEnvironments log config MUST nest under appLogsConfiguration; a bare
# top-level logAnalyticsConfiguration fails deploy with ManagedEnvironmentInvalidSchema.
if ($hasCAE -and ($iac -match 'logAnalyticsConfiguration') -and ($iac -notmatch 'appLogsConfiguration')) {
Add-Fail 'CAE-APPLOGS' "Container Apps Environment uses a bare logAnalyticsConfiguration β nest it under appLogsConfiguration.destination='log-analytics' (else ManagedEnvironmentInvalidSchema at deploy)" 'infra/modules'
}
# 14. CA-NO-REVISION-SUFFIX β a hardcoded revisionSuffix fails Phase 2 redeploy ('revision already exists').
if ($hasCA -and ($iac -match 'revisionSuffix\s*:')) {
Add-Fail 'CA-NO-REVISION-SUFFIX' 'Container App sets revisionSuffix β omit it (ARM auto-generates); a hardcoded value fails Phase 2 redeploy' 'infra/modules'
}
# 15. CA-IMAGE-PARAM β two-phase deploy needs `param containerImage` in main.bicep, else the Phase 2 `--parameters containerImage=` override is silently ignored and the placeholder image persists.
if ($hasCA -and $hasAcr -and ($mainBicep -notmatch 'param\s+containerImage')) {
Add-Fail 'CA-IMAGE-PARAM' 'main.bicep lacks `param containerImage` β the Phase 2 image override is silently ignored and the placeholder persists (MANIFEST_UNKNOWN)' 'infra/main.bicep'
}
# 16. ACRPULL-GUID β near-miss detector: the fixed AcrPull prefix present without the canonical full GUID = wrong last segment (hallucinated) β RoleDefinitionDoesNotExist. Cannot false-positive.
if (($iac -match '7f951dda-4ed3-4680-a7ca-') -and ($iac -notmatch '7f951dda-4ed3-4680-a7ca-43fe172d538d')) {
Add-Fail 'ACRPULL-GUID' 'AcrPull role GUID is wrong β canonical value is 7f951dda-4ed3-4680-a7ca-43fe172d538d (RoleDefinitionDoesNotExist otherwise)' 'infra/modules/role-assignments.bicep'
}
# 17. ACR-NO-PREMIUM-POLICY β retention/trust/quarantine are Premium-only; on Basic/Standard ACR they fail SkuNotSupported. FP-safe: skipped if any 'Premium' SKU appears in the IaC.
if ($hasAcr -and ($iac -match '(retentionPolicy|trustPolicy|quarantinePolicy)\s*:') -and ($iac -notmatch "'Premium'")) {
Add-Fail 'ACR-NO-PREMIUM-POLICY' 'ACR has a Premium-only policy (retention/trust/quarantine) but is not Premium β omit these for Basic/Standard (SkuNotSupported at deploy)' 'infra/modules'
}
}
# 9. WARN-FIXED β prereq warnings flagged fixPhase:"scaffold" must land in the generated IaC.
# Only warnings with a provable signal are enforced; unverifiable ones are skipped (no false BLOCK).
# Extend $warnSignal as new provable scaffold-phase warning IDs are added.
$prereq = Read-Json (Join-Path $SessionPath 'prereq-output.json')
if ($prereq -and $prereq.warnings) {
$warnSignal = @{
'W-PG-SSL' = @{ kind = 'iac'; pattern = 'PGSSLMODE|sslmode' } # SSL-mode env var emitted in IaC
'W-BUILDKIT' = @{ kind = 'file'; pattern = 'Dockerfile.azure' } # ACR-compatible Dockerfile emitted
}
foreach ($w in $prereq.warnings) {
if ($w.fixPhase -ne 'scaffold') { continue }
$sig = $warnSignal[$w.id]
if (-not $sig) { continue } # no deterministic signal β cannot verify, do not block
$present = if ($sig.kind -eq 'file') {
[bool](Get-ChildItem -Recurse -Filter $sig.pattern -ErrorAction SilentlyContinue | Select-Object -First 1)
} else {
[bool]($iac -match $sig.pattern)
}
if (-not $present) {
Add-Fail 'WARN-FIXED' "prereq warning '$($w.id)' (fixPhase:scaffold) fix not found in IaC β $($w.fix)" 'infra/'
}
}
}
$result = [ordered]@{ passed = ($failures.Count -eq 0); failures = $failures }
$result | ConvertTo-Json -Depth 5 -Compress
if ($failures.Count -gt 0) { exit 1 } else { exit 0 }
scaffold-conformance.sh 13.7 KB
#!/usr/bin/env bash
# Scaffold -> Deploy conformance gate (bash twin of scaffold-conformance.ps1).
# Checks generated IaC against the plan for semantic defects `az bicep build` cannot catch.
#
# Usage: scaffold-conformance.sh <sessionPath> [infraPath]
# Output: JSON { passed, failures:[{id,detail,file}] } to stdout.
# Exit: 0 = pass, 1 = one or more BLOCK failures.
#
# Pure-text checks (tags, TLS, purge, deployer role, plaintext secret) always run.
# Plan-dependent checks (version, services-complete, db-name, warn-fixed) run only when `jq` is present.
set -u
SESSION_PATH="${1:?usage: scaffold-conformance.sh <sessionPath> [infraPath]}"
INFRA_PATH="${2:-infra}"
PLAN="$SESSION_PATH/prepare-plan.json"
MAIN="$INFRA_PATH/main.bicep"
PARAMS="$INFRA_PATH/main.parameters.json"
main_bicep="$( [ -f "$MAIN" ] && cat "$MAIN" || true )"
params_raw="$( [ -f "$PARAMS" ] && cat "$PARAMS" || true )"
iac="$( find "$INFRA_PATH" -name '*.bicep' -type f 2>/dev/null -exec cat {} + || true )"
HAVE_JQ=0; command -v jq >/dev/null 2>&1 && HAVE_JQ=1
# failures accumulator (JSON objects, newline-separated)
fails=""
add_fail() { # id detail file
local obj; obj="$(printf '{"id":"%s","detail":"%s","file":"%s"}' "$1" "$2" "$3")"
fails="${fails:+$fails,}$obj"
}
iac_has() { printf '%s' "$iac" | grep -qE "$1"; }
# 1. TAGS-NO-CAMEL
if printf '%s' "$main_bicep" | grep -qE 'appOnboard(Skill|SessionId)|createdAt:|deployedBy:'; then
add_fail "TAGS-NO-CAMEL" "camelCase tag keys found β use hyphenated ('app-onboard-skill' ...)" "infra/main.bicep"
fi
# 12. DIAG-GATED β we don't scaffold diagnostic-settings; if present it must be gated behind enableDiagnostics (default false) or removed. Unconditional wiring blocks the first deploy.
# Match an actual diagnostic module/resource declaration, not a prose comment mentioning 'diagnostic'.
if printf '%s' "$main_bicep" | grep -qE 'module[[:space:]]+[A-Za-z]*[Dd]iagnostic|Microsoft\.Insights/diagnosticSettings' && ! printf '%s' "$main_bicep" | grep -q 'enableDiagnostics'; then
add_fail "DIAG-GATED" "diagnostic-settings module wired without an enableDiagnostics gate β gate it (module ... = if (enableDiagnostics), default false) or remove it; unconditional wiring blocks the first deploy" "infra/main.bicep"
fi
# 2. NO-PLAINTEXT-SECRET (jq-free: awk over the params file so it runs on hosts without jq)
if [ -n "$params_raw" ]; then
bad_keys="$(printf '%s' "$params_raw" | awk '
{ line=tolower($0) }
line ~ /"[a-z0-9_]*(password|secret|connstring|connection)[a-z0-9_]*"[ \t]*:/ {
n=split($0, parts, "\"")
for (i=1;i<=n;i++) { if (tolower(parts[i]) ~ /(password|secret|connstring|connection)/) { pend=parts[i]; break } }
}
pend != "" && line ~ /"value"[ \t]*:[ \t]*"[^"]+"/ { print pend; pend="" }
/}/ { pend="" }
')"
if [ -n "$bad_keys" ]; then
while IFS= read -r k; do
[ -z "$k" ] && continue
add_fail "NO-PLAINTEXT-SECRET" "parameter '$k' has a literal value β must be @secure(), passed at deploy" "infra/main.parameters.json"
done <<< "$bad_keys"
fi
fi
# 2b. NO-BICEP-LITERAL-SECRET (pure-text) β secret values must be @secure() params, never quoted literals.
# Property name must end in 'password'/'connectionString' right before ':' (so 'secretName' etc. never match).
if [ -n "$iac" ]; then
if printf '%s' "$iac" | grep -qiE "[A-Za-z]*(password|connectionstring)[[:space:]]*:[[:space:]]*'[^']+'"; then
k="$(printf '%s' "$iac" | grep -oiE "[A-Za-z]*(password|connectionstring)[[:space:]]*:[[:space:]]*'[^']+'" | head -n1 | sed -E "s/[[:space:]]*:.*//")"
add_fail "NO-BICEP-LITERAL-SECRET" "property '$k' assigned a literal secret in Bicep β use an @secure() param passed at deploy" "infra/"
fi
if printf '%s' "$iac" | grep -qE ":[[:space:]]*'[^']*(Password=|AccountKey=|SharedAccessKey=|://[^:@/ ]+:[^:@/ ]+@)[^']*'"; then
add_fail "NO-BICEP-LITERAL-SECRET" "quoted literal contains an embedded credential (Password=/AccountKey=/user:pass@) in Bicep β use an @secure() param" "infra/"
fi
fi
# service-name -> resource-type map (longest keys first so 'container apps environment' wins)
declare -a MAP_KEYS=( "container apps environment" "container app" "container registry" "mysql" "postgres" \
"key vault" "log analytics" "application insights" "static web app" "app service" "functions" \
"sql" "cosmos" "redis" "storage" "service bus" "event hub" )
map_type() {
case "$1" in
*"container apps environment"*) echo 'Microsoft\.App/managedEnvironments';;
*"container app"*) echo 'Microsoft\.App/containerApps';;
*"container registry"*) echo 'Microsoft\.ContainerRegistry/registries';;
*mysql*) echo 'Microsoft\.DBforMySQL/flexibleServers';;
*postgres*) echo 'Microsoft\.DBforPostgreSQL/flexibleServers';;
*"key vault"*) echo 'Microsoft\.KeyVault/vaults';;
*"log analytics"*) echo 'Microsoft\.OperationalInsights/workspaces';;
*"application insights"*) echo 'Microsoft\.Insights/components';;
*"static web app"*) echo 'Microsoft\.Web/staticSites';;
*"app service"*|*functions*) echo 'Microsoft\.Web/sites';;
*sql*) echo 'Microsoft\.Sql/servers';;
*cosmos*) echo 'Microsoft\.DocumentDB/databaseAccounts';;
*redis*) echo 'Microsoft\.Cache/redis';;
*storage*) echo 'Microsoft\.Storage/storageAccounts';;
*"service bus"*) echo 'Microsoft\.ServiceBus/namespaces';;
*"event hub"*) echo 'Microsoft\.EventHub/namespaces';;
*) echo '';;
esac
}
svc_names=""; db_version=""; app_db_name=""; has_mysql=0; has_pg=0
if [ "$HAVE_JQ" = 1 ] && [ -f "$PLAN" ]; then
svc_names="$(jq -r '.services[].name' "$PLAN" 2>/dev/null)"
app_db_name="$(jq -r '.appDbName // empty' "$PLAN" 2>/dev/null)"
printf '%s' "$svc_names" | grep -qiE 'mysql' && has_mysql=1
printf '%s' "$svc_names" | grep -qiE 'postgres' && has_pg=1
# 3. SERVICES-COMPLETE
while IFS= read -r name; do
[ -z "$name" ] && continue
lc="$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]')"
expected="$(map_type "$lc")"
if [ -n "$expected" ] && ! iac_has "$expected"; then
add_fail "SERVICES-COMPLETE" "planned service '$name' has no matching resource ($expected) in IaC" "infra/"
fi
done < <(printf '%s\n' "$svc_names")
else
# jq absent: fall back to grep for the DB-type presence (drives TLS/name checks below)
printf '%s' "$iac" | grep -qE 'Microsoft\.DBforMySQL/flexibleServers' && has_mysql=1
printf '%s' "$iac" | grep -qE 'Microsoft\.DBforPostgreSQL/flexibleServers' && has_pg=1
fi
# --- Managed database checks ---
if [ "$has_mysql" = 1 ] || [ "$has_pg" = 1 ]; then
# 4. DB-VERSION-MATCH β each server's version must equal its capabilities-verified plan value.
# Scope the match to the DB resource block so an unrelated 'version:' elsewhere isn't picked up.
if [ "$HAVE_JQ" = 1 ] && [ -f "$PLAN" ]; then
while IFS=$'\t' read -r dbkind dbver; do
[ -z "$dbver" ] && continue
case "$dbkind" in
*MySQL*) marker='Microsoft[.]DBforMySQL/flexibleServers';;
*) marker='Microsoft[.]DBforPostgreSQL/flexibleServers';;
esac
iac_ver="$(printf '%s\n' "$iac" | awk -v marker="$marker" '
$0 ~ marker { armed=1 }
armed==1 && match($0, /version:[ \t]*'"'"'[^'"'"']+'"'"'/) {
v=substr($0,RSTART,RLENGTH); sub(/version:[ \t]*'"'"'/,"",v); sub(/'"'"'.*/,"",v); print v; exit
}')"
if [ -n "$iac_ver" ] && [ "$iac_ver" != "$dbver" ]; then
add_fail "DB-VERSION-MATCH" "$dbkind IaC version '$iac_ver' != plan '$dbver'" "infra/modules"
fi
done < <(jq -r '.services[]? | select(.name|test("MySQL|PostgreSQL")) | [.name, (.version // "")] | @tsv' "$PLAN" 2>/dev/null)
fi
# 5. DB-TLS-ON (MySQL, pure-text)
if [ "$has_mysql" = 1 ] && ! iac_has 'require_secure_transport'; then
add_fail "DB-TLS-ON" "MySQL module missing require_secure_transport config" "infra/modules"
fi
# 6. DB-NAME-PRESENT (needs plan.appDbName from jq)
if [ -n "$app_db_name" ] && ! iac_has 'flexibleServers/databases'; then
add_fail "DB-NAME-PRESENT" "app DB '$app_db_name' declared but no flexibleServers/databases resource" "infra/modules"
fi
# 10. MYSQL-NO-NETWORK-BLOCK (MySQL, pure-text) β public flow must omit the network block;
# an empty delegatedSubnetResourceId is rejected by ARM (LinkedInvalidPropertyId).
if [ "$has_mysql" = 1 ] && iac_has 'delegatedSubnetResourceId|privateDnsZoneResourceId'; then
add_fail "MYSQL-NO-NETWORK-BLOCK" "MySQL module includes a network block (delegatedSubnetResourceId) β omit it for public access; an empty value is rejected by ARM" "infra/modules"
fi
# 11. DB-LOGIN-NOT-RESERVED (pure-text) β administratorLogin must not be an Azure-reserved name.
if iac_has "administratorLogin:[[:space:]]*'(root|admin|administrator|guest|public|sa|azure_superuser|azure_pg_admin)'"; then
resv="$(printf '%s' "$iac" | grep -oE "administratorLogin:[[:space:]]*'[^']+'" | grep -oiE "(root|admin|administrator|guest|public|sa|azure_superuser|azure_pg_admin)" | head -n1)"
add_fail "DB-LOGIN-NOT-RESERVED" "administratorLogin uses a reserved name ('${resv}') β derive a safe login (e.g. '{project}admin'), never a compose-sourced reserved name" "infra/modules"
fi
fi
# --- Key Vault checks (pure-text) ---
if iac_has 'Microsoft\.KeyVault/vaults'; then
# 7. KV-NO-PURGE
if iac_has 'enablePurgeProtection'; then
add_fail "KV-NO-PURGE" "enablePurgeProtection present β omit it (policy may reject false)" "infra/modules"
fi
# 8. KV-DEPLOYER-ROLE
has_officer=0; iac_has 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7' && has_officer=1
has_param=0; { iac_has 'deployerObjectId' || printf '%s' "$params_raw" | grep -q 'deployerObjectId'; } && has_param=1
if [ "$has_officer" != 1 ] || [ "$has_param" != 1 ]; then
add_fail "KV-DEPLOYER-ROLE" "missing deployer Key Vault Secrets Officer role and/or deployerObjectId param" "infra/modules/role-assignments.bicep"
fi
fi
# --- Container Apps / ACR checks (deterministic ARM-failure invariants) ---
if [ -n "$iac" ]; then
has_ca=0; iac_has 'Microsoft\.App/containerApps' && has_ca=1
has_cae=0; iac_has 'Microsoft\.App/managedEnvironments' && has_cae=1
has_acr=0; iac_has 'Microsoft\.ContainerRegistry/registries' && has_acr=1
# 13. CAE-APPLOGS β managedEnvironments log config MUST nest under appLogsConfiguration; a bare
# top-level logAnalyticsConfiguration fails deploy with ManagedEnvironmentInvalidSchema.
if [ "$has_cae" = 1 ] && iac_has 'logAnalyticsConfiguration' && ! iac_has 'appLogsConfiguration'; then
add_fail "CAE-APPLOGS" "Container Apps Environment uses a bare logAnalyticsConfiguration β nest it under appLogsConfiguration.destination='log-analytics' (else ManagedEnvironmentInvalidSchema at deploy)" "infra/modules"
fi
# 14. CA-NO-REVISION-SUFFIX β a hardcoded revisionSuffix fails Phase 2 redeploy ('revision already exists').
if [ "$has_ca" = 1 ] && iac_has 'revisionSuffix[[:space:]]*:'; then
add_fail "CA-NO-REVISION-SUFFIX" "Container App sets revisionSuffix β omit it (ARM auto-generates); a hardcoded value fails Phase 2 redeploy" "infra/modules"
fi
# 15. CA-IMAGE-PARAM β two-phase deploy needs 'param containerImage' in main.bicep, else the Phase 2 override is silently ignored and the placeholder image persists.
if [ "$has_ca" = 1 ] && [ "$has_acr" = 1 ] && ! printf '%s' "$main_bicep" | grep -qE 'param[[:space:]]+containerImage'; then
add_fail "CA-IMAGE-PARAM" "main.bicep lacks 'param containerImage' β the Phase 2 image override is silently ignored and the placeholder persists (MANIFEST_UNKNOWN)" "infra/main.bicep"
fi
# 16. ACRPULL-GUID β near-miss detector: fixed AcrPull prefix present without the canonical full GUID = hallucinated last segment β RoleDefinitionDoesNotExist. Cannot false-positive.
if iac_has '7f951dda-4ed3-4680-a7ca-' && ! iac_has '7f951dda-4ed3-4680-a7ca-43fe172d538d'; then
add_fail "ACRPULL-GUID" "AcrPull role GUID is wrong β canonical value is 7f951dda-4ed3-4680-a7ca-43fe172d538d (RoleDefinitionDoesNotExist otherwise)" "infra/modules/role-assignments.bicep"
fi
# 17. ACR-NO-PREMIUM-POLICY β retention/trust/quarantine are Premium-only; on Basic/Standard ACR they fail SkuNotSupported. FP-safe: skipped if any 'Premium' SKU appears.
if [ "$has_acr" = 1 ] && iac_has '(retentionPolicy|trustPolicy|quarantinePolicy)[[:space:]]*:' && ! iac_has "'Premium'"; then
add_fail "ACR-NO-PREMIUM-POLICY" "ACR has a Premium-only policy (retention/trust/quarantine) but is not Premium β omit these for Basic/Standard (SkuNotSupported at deploy)" "infra/modules"
fi
fi
# 9. WARN-FIXED (needs jq to read prereq-output.json warnings[]) β prereq warnings flagged
# fixPhase:"scaffold" must land in the IaC. Only warnings with a provable signal are enforced;
# unverifiable ones are skipped (no false BLOCK). Extend the case map for new provable warning IDs.
PREREQ="$SESSION_PATH/prereq-output.json"
if [ "$HAVE_JQ" = 1 ] && [ -f "$PREREQ" ]; then
while IFS=$'\t' read -r wid wfix; do
[ -z "$wid" ] && continue
present=0
case "$wid" in
W-PG-SSL) printf '%s' "$iac" | grep -qiE 'PGSSLMODE|sslmode' && present=1 ;;
W-BUILDKIT) [ -n "$(find . -name 'Dockerfile.azure' -type f 2>/dev/null | head -n1)" ] && present=1 ;;
*) continue ;; # no deterministic signal β cannot verify, do not block
esac
if [ "$present" != 1 ]; then
add_fail "WARN-FIXED" "prereq warning '$wid' (fixPhase:scaffold) fix not found in IaC β $wfix" "infra/"
fi
done < <(jq -r '.warnings[]? | select(.fixPhase=="scaffold") | [.id, .fix] | @tsv' "$PREREQ" 2>/dev/null)
fi
if [ -z "$fails" ]; then
printf '{"passed":true,"failures":[]}\n'
exit 0
else
printf '{"passed":false,"failures":[%s]}\n' "$fails"
exit 1
fi
License (MIT)
View full license text
MIT License Copyright 2025 (c) Microsoft Corporation. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.