──────
Build and Push Docker Images / detect-changes (push) Successful in 11s
Build and Push Docker Images / build-auth-api (push) Successful in 1m0s
Build and Push Docker Images / build-employee-api (push) Successful in 1m2s
Build and Push Docker Images / build-transit-api (push) Successful in 1m1s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m5s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m13s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m15s
Build and Push Docker Images / deploy-test-node (push) Successful in 3s
Build and Push Docker Images / detect-changes (push) Successful in 11s
Build and Push Docker Images / build-auth-api (push) Successful in 1m0s
Build and Push Docker Images / build-employee-api (push) Successful in 1m2s
Build and Push Docker Images / build-transit-api (push) Successful in 1m1s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m5s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m13s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m15s
Build and Push Docker Images / deploy-test-node (push) Successful in 3s
### Summary of Changes
1. Documentation Consolidation:
• Relocated the docs/ folder and all repository .md files (README.md, DEPLOYMENT.md, DOCKER_README.md,
DOMAINS_README.md, GETTING_STARTED.md, CI_CD_PORTAINER_GUIDE.md, ADMIN_DEPLOYMENT_SUMMARY.md,
SECURITY_PRIVATE_FILES.md, handoff.md, azure-project-plan.md, plus service-specific READMEs) into .
• Updated document links in README.md.
2. AI Agent Project Map:
• Created PROJECT_MAP.md in the project root containing complete architecture diagrams, service specs, ports, DB
schemas, environment variables, and developer workflows.
• Added PROJECT_MAP.md to .gitignore.
3. Monitoring UI & Infrastructure Integration:
• Added monitoring_ui service to docker-compose.yml (port 5175).
• Configured Gitea workflow docker-build.yml for monitoring_ui builds.
• Added CORS middleware headers in index.js.
• Added nginx.conf copy step in Dockerfile.
──────
### Recommended Commit Message
docs: consolidate project documentation into monitoring_ui and add agent project map
- Move all markdown documentation files and docs/ folder into monitoring_ui/docs/
- Update documentation hub index (monitoring_ui/docs/README.md) with relative paths
- Generate comprehensive AI agent reference map (PROJECT_MAP.md) in repository root
- Add PROJECT_MAP.md and monitoring_ui build output/env overrides to .gitignore
- Integrate monitoring_ui service into docker-compose.yml and Gitea CI workflow
- Configure CORS handling in employee_api for monitoring dashboard access
──────
### Alternative Short Commit Message
refactor(docs): move docs to monitoring_ui and create AI agent PROJECT_MAP.md
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# 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
|
||||
|
||||
The compute floor is **B1 (Basic, ~$13/mo)** — F1/D1/Free are never selected (managed identity is mandatory and the free-tier MI sidecar OOMs). When native modules, TypeScript build, large deps, or a WSGI/ASGI server push resource needs higher, size up from B1 (B2/S1) and surface the reason at the approval gate: "⚠️ {reason}. {sku} required." Never size below B1.
|
||||
|
||||
### 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.
|
||||
@@ -0,0 +1,87 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# 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.
|
||||
@@ -0,0 +1,64 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# 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`, `st`, `rg`)
|
||||
|
||||
**Examples with `resourcePrefix = myapp-dev-a1d5`:**
|
||||
- Resource Group: `rg-myapp-dev-a1d5`
|
||||
- App Service: `app-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` |
|
||||
| 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 — Storage Account and Container App (24 / 32 chars, alphanumeric) are the tightest constraints. Budget for `{project}`: `24 - len(abbreviation) - len(env) - len(suffix) - 3 - 2` (separators + healing reserve). For `st` + `dev` (Storage strips hyphens): ~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
|
||||
@@ -0,0 +1,201 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Prepare artifact schema — prepare-plan.json.
|
||||
* Read by prepare instructions.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", "S1") */
|
||||
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[];
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# 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 |
|
||||
|-----|-------------|-------|
|
||||
| B1 | `B1` | Compute floor — no F1/Free (see [sku-matrix.md](sku-matrix.md)) |
|
||||
| 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`
|
||||
|
||||
---
|
||||
|
||||
### 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/):
|
||||
- **Standard tier (floor — Free is never selected):** Flat monthly per app, unlimited bandwidth, 5 custom domains, SLA, private endpoints
|
||||
|
||||
---
|
||||
|
||||
### 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).
|
||||
@@ -0,0 +1,73 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# 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."`
|
||||
|
||||
## No Free-Tier Shortcut — Compute Always Has Cost
|
||||
|
||||
> ⛔ **There is no $0 shortcut.** Compute has a floor (App Service **B1**, Static Web Apps **Standard**, Functions **Flex Consumption**) — F1/D1/Free/Consumption are never selected (see [sku-matrix.md](sku-matrix.md)). So any app with a compute component has a non-zero monthly cost and MUST get a live pricing call. Do NOT write a $0 estimate for a compute app.
|
||||
|
||||
Some services can still fall within free **grant** limits (they are metered, not a fixed free SKU) — note the grant in the estimate but still price the paid meters:
|
||||
|
||||
| Service | Free grant (still price paid usage above it) |
|
||||
|---------|-----------------------------------------------|
|
||||
| Functions (Flex Consumption) | First 250K executions + 100K GB-s/month |
|
||||
| Cosmos DB | Free-tier account: 1000 RU/s + 25 GB (one per subscription) |
|
||||
| Container Apps | First 180K vCPU-s + 360K GiB-s + 2M requests/month |
|
||||
| Log Analytics | First 5 GB/month ingestion |
|
||||
|
||||
---
|
||||
|
||||
## 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) 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."
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# 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
|
||||
|
||||
> ⛔ **Existing framework beats generic component type.** If a component path contains a `host.json` file
|
||||
> and an Azure Functions SDK or worker configuration, select Azure Functions for that component. Do not
|
||||
> route it through the generic REST/GraphQL API row, merge it into a frontend service, or package it as an
|
||||
> SWA-managed API. When a SPA and Functions component coexist, `services[]` must contain separate frontend
|
||||
> and Azure Functions entries whose `component` values match the detected component names.
|
||||
|
||||
> ⛔ **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 |
|
||||
| Existing Azure Functions project | Azure Functions (Flex Consumption) | Azure Functions Premium | `host.json` plus Functions SDK or worker configuration |
|
||||
| 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.
|
||||
|
||||
**Frontend plus Functions:** Default to a detached frontend host and a separately deployed Function App.
|
||||
For Static Web Apps, configure the Function App CORS allowlist with the SWA origin. Do not change the
|
||||
Function App's runtime, authentication provider, or app settings to `azureStaticWebApps`. Only link the
|
||||
existing Function App as an SWA backend when `context.json.overrides[]` records explicit user approval for
|
||||
`linkFunctionsToStaticWebApp`; linking does not remove the Function App service or change its deployment
|
||||
channel.
|
||||
|
||||
**AKS vs Container Apps:** Use Container Apps when scale-to-zero needed, no K8s expertise, or KEDA-driven event processing. Delegate AKS planning to the `azure-kubernetes` agent.
|
||||
|
||||
**App Service vs Container Apps:** App Service preferred for single-process apps with no container orchestration (B1 floor, ~$13/mo — no free tier is offered). 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 scales to zero (near-$0 idle) — the closest thing to free, and it supports managed identity.
|
||||
|
||||
**Static Dockerfile sites (nginx/httpd serving HTML):**
|
||||
- **Primary:** Static Web Apps **Standard** — global CDN, custom auth/CORS, managed-identity BYO backends.
|
||||
> ⛔ **SWA region availability:** Validate via `az provider show --namespace Microsoft.Web --query "resourceTypes[?resourceType=='staticSites'].locations" -o tsv`.
|
||||
- **Alternative:** App Service **B1** — ⛔ for static content use Windows B1 (IIS serves `index.html` natively) or Linux B1 (`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):** Static Web Apps Standard preferred, or Windows App Service B1 (IIS serves natively; Linux: `linuxFxVersion: 'STATICSITE|1.0'`).
|
||||
|
||||
## 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 |
|
||||
| Managed Identity | Service-to-service auth (zero secrets) |
|
||||
|
||||
## Specialized Routing
|
||||
|
||||
Check before mapping — delegate to specialized agent 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` | On-compute app secret (App Service app settings / CA native secrets) — ⛔ no Key Vault | App-internal secrets only; Azure resource auth uses managed identity |
|
||||
| `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` | On-compute app secret (App Service app settings / CA native secrets) — ⛔ no Key Vault | App-internal secrets only; Azure resource auth uses managed identity |
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,46 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# SKU Selection Matrix
|
||||
|
||||
Select SKU based on `context.json.intent.budget`. Cross-reference with `intent.scale` for right-sizing.
|
||||
|
||||
## Budget Tiers
|
||||
|
||||
> ⛔ **Free/shared tiers (App Service F1/D1, Static Web Apps Free) are NOT offered.** Every app requires a managed identity for Azure resource auth, and the free-tier MI sidecar OOMs on Linux; free SWA also lacks CORS/custom-auth config these apps need. The compute floor is **B1 (App Service)** / **Standard (Static Web Apps)**. Never emit F1, D1, or Free.
|
||||
|
||||
| Service | cost-optimized | balanced | performance |
|
||||
|---------|---------------|----------|-------------|
|
||||
| Container Apps | Consumption (scale-to-zero) | Consumption | Dedicated |
|
||||
| App Service | B1 (Basic) | 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 | Standard | Standard | Standard |
|
||||
| Service Bus | Basic | Standard | Premium |
|
||||
| Redis Cache | Basic C0 | Standard C1 | Premium P1 |
|
||||
| Functions | Flex 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.
|
||||
|
||||
> ⛔ **Functions floor is Flex Consumption — NOT Consumption or Elastic Premium.** The Consumption and Elastic Premium plans back `AzureWebJobsStorage` with Azure Files, which does **not** support managed-identity connections and therefore **requires `allowSharedKeyAccess` on the storage account** — banned by the managed-identity-only rule (and the KV workaround for the Files connection string is also banned). **Flex Consumption** supports identity-based `AzureWebJobsStorage` (`AzureWebJobsStorage__accountName` + `AzureWebJobsStorage__credential=managedidentity`), so it is the only Functions plan compatible with `allowSharedKeyAccess: false`. Never select Consumption/Elastic Premium — this is the same floor-raise as App Service F1→B1.
|
||||
|
||||
## 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 (never below the B1 / SWA Standard floor), 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 at or above the floor (App Service B1, Static Web Apps Standard)
|
||||
- Simple web app with no DB → ~$13–$20/month (B1 minimum — no $0 tier)
|
||||
- Always output exact SKU codes: "App Service B1 (Basic)" not "cheapest 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. Pick **App Service B1 (Basic)** for dynamic apps that need a runtime (Node.js/Python starter templates), or **Static Web Apps Standard** 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". Do NOT ask the user about budget unless they mention cost first (per [intent-gathering.md](../../references/intent-gathering.md)).
|
||||
|
||||
⛔ **There is no free promise — fast-track starts at the B1 / SWA Standard floor.** If the chosen floor SKU is unavailable during deploy prep (no B1 quota — see [sku-quota-validation.md](sku-quota-validation.md) § After Checking), fall back to the **cheapest AVAILABLE** tier at or above the floor. Record an `assumptions[]` note stating WHY the SKU differs, so the approval gate presents the resulting cost.
|
||||
@@ -0,0 +1,108 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# 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 "SKU 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. SWA **Standard** is the floor (Free is never selected — see [sku-matrix.md](sku-matrix.md)); Standard has no per-subscription app cap to check. No quota gate needed.
|
||||
|
||||
**Storage** — default limit 250 accounts/region. Rarely exhausted — skip programmatic check unless the plan requires multiple storage accounts.
|
||||
|
||||
### Interpret Results
|
||||
|
||||
- `available > 0` → AVAILABLE. `available = 0` / `limit <= 0` → BLOCKED (a `limit=0`, `used=-1` response is the API sentinel for "SKU 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. **Floor SKU missing in requested region but present elsewhere** → present BOTH, ranked by cost: (a) the floor SKU (B1 / SWA Standard / Flex Consumption) in the nearest confirmed region, (b) the cheapest available SKU IN the requested region. Both show monthly cost + an `assumptions[]` note. User picks — never silently relocate (region may be a data-residency/latency requirement).
|
||||
2. **Floor SKU unavailable in ALL regions** → step down the fallback ladder to the **cheapest available** tier at or above the floor (never below B1 / SWA Standard / Flex Consumption; let live quota decide the exact SKU):
|
||||
- No option at the floor → the cheapest available higher tier the app supports. Add an `assumptions[]` note stating why (e.g., "No B1 quota in {checkedRegions}; selected {sku}").
|
||||
- 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.
|
||||
@@ -0,0 +1,56 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# 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-grant handling, 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 other agents or hand off** — no external agent calls of any kind. 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).
|
||||
- ⛔ **No $0 shortcut — compute always has cost** (App Service B1 / SWA Standard / Functions Flex Consumption floor). Price every compute app via live API; note free **grants** (Cosmos free-tier account, Container Apps/Functions grants) but still price paid meters. Do NOT return $0 for a compute app.
|
||||
- ⛔ `armSkuName` is case-sensitive — use `B1` not `b1`
|
||||
- ⛔ For services with empty `armSkuName` (Container Apps, Functions Flex Consumption, ACR, Storage, Cosmos): 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 in the query
|
||||
|
||||
## Token Budget
|
||||
|
||||
≤500 tokens for cost estimate report.
|
||||
@@ -0,0 +1,74 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# 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 other agents or hand off** — no external agent calls of any kind. 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
|
||||
|
||||
- ⛔ Paid ≠ unlimited — B1, Flex Consumption, Serverless all have per-subscription, per-region quotas (F1/Free are never selected — floor is B1)
|
||||
- ⛔ 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.
|
||||
@@ -0,0 +1,26 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Validation Rubric
|
||||
|
||||
Run all 4 dimensions before writing `prepare-plan.json`. All must pass — a failure in any dimension triggers the [Error Handling](../instructions.md#error-handling) procedures.
|
||||
|
||||
## Dimensions
|
||||
|
||||
| Dimension | Pass Criteria |
|
||||
|-----------|---------------|
|
||||
| **Goal Alignment** | Every `context.json.intent` field reflected in service/SKU choice. No orphaned or omitted components: every detected deployable component maps to its own compute service or an explicitly approved shared host. Every detected Azure Functions component maps to Azure Functions, even when paired with a Static Web Apps frontend. `overrides[]` honored. |
|
||||
| **WAF Alignment** | **Cost:** SKU matches budget; alternatives document cost tradeoffs. **Reliability:** Production plans include zone-redundant SKUs, GRS storage. **Security:** Managed identity everywhere; app-internal secrets stored on-compute (⛔ no 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; App Service → App Insights for monitoring; all services → managed identity + RBAC/data-plane role for auth — databases are Entra-only, no connection-string password). 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 free/shared SKUs (compute floor is B1 / SWA Standard / Functions Flex Consumption). |
|
||||
|
||||
## 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](../instructions.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
|
||||
Reference in New Issue
Block a user