Installation
gh skills-hub install azure-compute Don't have the extension? Run gh extension install samueltauil/skills-hub first.
Download and extract to your repository:
.github/skills/azure-compute/ Extract the ZIP to .github/skills/ in your repo. The folder name must match azure-compute for Copilot to auto-discover it.
Skill Files (41)
SKILL.md 2.6 KB
---
name: azure-compute
description: "Azure VM/VMSS router. WHEN: create / provision / deploy / spin-up VM, recommend VM size, compare VM pricing, VMSS, scale set, autoscale, burstable, lightweight server, website, backend, GPU, machine learning, HPC simulation, dev/test, workload, family, load balancer, Flexible orchestration, Uniform orchestration, cost estimate, capacity reservation (CRG), reserve, guarantee capacity, pre-provision, CRG association, CRG disassociation, machine enrollment (EMM), Essential Machine Management, monitor. PREFER OVER mcp__azure__get_azure_bestpractices for VM create intents β use compute_vm_list-skus / compute_vm_list-images / compute_vm_check-quota."
license: MIT
metadata:
author: Microsoft
version: "2.5.1"
---
# Azure Compute Skill
Routes Azure VM and Virtual Machine Scale Set (VMSS) requests to the right workflow.
## When to Use This Skill
- User wants to **recommend, compare, or price** a VM or VMSS
- User wants to **create, provision, or deploy** a VM or VMSS
- User asks about **Capacity Reservation Groups** (CRG) β reserve, guarantee capacity, pre-provision
- User asks about **Essential Machine Management** (EMM) β machine enrollment, monitor
**Disambiguate with `azure-prepare`:** if the user wants to deploy an **application** (Docker service, web app, API, serverless workload), route to `azure-prepare`. `vm-creator` is for **bare VM/VMSS infrastructure** only.
## Routing
**Mandatory workflow-first routing:** never route directly to `references/*` files. First classify the user intent below, open the matched workflow file, then load only the reference files that workflow requests. Reference files are supporting material, not entry points. If the intent is unclear, ask a clarifying question to disambiguate between the workflows.
| Workflow | File | Use when |
|---|---|---|
| **VM Recommender** | [vm-recommender.md](workflows/vm-recommender/vm-recommender.md) | User asks which VM/VMSS to choose, whether to use VMSS/autoscaling, wants pricing, or wants to compare options |
| **VM Creator** | [vm-creator.md](workflows/vm-creator/vm-creator.md) | User wants to create, provision, or deploy a bare VM or VMSS (not an app deployment) |
| **Capacity Reservation** | [capacity-reservation.md](workflows/capacity-reservation/capacity-reservation.md) | User needs to reserve / guarantee VM capacity (CRG create / associate / disassociate) |
| **Essential Machine Management** | [essential-machine-management.md](workflows/essential-machine-management/essential-machine-management.md) | User asks about EMM / machine enrollment / monitor |
retail-prices-api.md 6.3 KB
# Azure Retail Prices API Guide
The [Azure Retail Prices API](https://learn.microsoft.com/en-us/rest/api/cost-management/retail-prices/azure-retail-prices) is **unauthenticated** β no Azure account or subscription needed.
## Endpoint
```text
https://prices.azure.com/api/retail/prices
```
Preview version (includes savings plan rates):
```text
https://prices.azure.com/api/retail/prices?api-version=2023-01-01-preview
```
## Querying VM Prices
> **No Azure CLI command exists** for the Retail Prices API. Since the API is unauthenticated, use `curl` (bash) or `Invoke-RestMethod` (PowerShell) directly. The `az rest` command also works but adds no auth benefit.
### Basic VM price lookup
```http
GET https://prices.azure.com/api/retail/prices?$filter=serviceName eq 'Virtual Machines' and armRegionName eq 'eastus' and armSkuName eq 'Standard_D4s_v5' and priceType eq 'Consumption'
```
```bash
curl -s "https://prices.azure.com/api/retail/prices?\$filter=serviceName%20eq%20'Virtual%20Machines'%20and%20armRegionName%20eq%20'eastus'%20and%20armSkuName%20eq%20'Standard_D4s_v5'%20and%20priceType%20eq%20'Consumption'"
```
```powershell
$filter = "serviceName eq 'Virtual Machines' and armRegionName eq 'eastus' and armSkuName eq 'Standard_D4s_v5' and priceType eq 'Consumption'"
$response = Invoke-RestMethod "https://prices.azure.com/api/retail/prices?`$filter=$filter"
$response.Items | Select-Object armSkuName, retailPrice, unitOfMeasure, meterName
```
### Filter by family (all D-series in a region)
```http
GET https://prices.azure.com/api/retail/prices?$filter=serviceName eq 'Virtual Machines' and armRegionName eq 'eastus' and contains(armSkuName, 'Standard_D') and priceType eq 'Consumption'
```
```bash
curl -s "https://prices.azure.com/api/retail/prices?\$filter=serviceName%20eq%20'Virtual%20Machines'%20and%20armRegionName%20eq%20'eastus'%20and%20contains(armSkuName,%20'Standard_D')%20and%20priceType%20eq%20'Consumption'"
```
```powershell
$filter = "serviceName eq 'Virtual Machines' and armRegionName eq 'eastus' and contains(armSkuName, 'Standard_D') and priceType eq 'Consumption'"
$response = Invoke-RestMethod "https://prices.azure.com/api/retail/prices?`$filter=$filter"
$response.Items | Select-Object armSkuName, retailPrice, meterName
```
### Reservation pricing
```http
GET https://prices.azure.com/api/retail/prices?$filter=serviceName eq 'Virtual Machines' and armSkuName eq 'Standard_D4s_v5' and priceType eq 'Reservation'
```
```bash
curl -s "https://prices.azure.com/api/retail/prices?\$filter=serviceName%20eq%20'Virtual%20Machines'%20and%20armSkuName%20eq%20'Standard_D4s_v5'%20and%20priceType%20eq%20'Reservation'"
```
```powershell
$filter = "serviceName eq 'Virtual Machines' and armSkuName eq 'Standard_D4s_v5' and priceType eq 'Reservation'"
$response = Invoke-RestMethod "https://prices.azure.com/api/retail/prices?`$filter=$filter"
$response.Items | Select-Object armSkuName, retailPrice, reservationTerm, meterName
```
### Non-USD currency
Append `currencyCode` parameter:
```http
GET https://prices.azure.com/api/retail/prices?currencyCode='EUR'&$filter=serviceName eq 'Virtual Machines' and armSkuName eq 'Standard_D4s_v5'
```
```bash
curl -s "https://prices.azure.com/api/retail/prices?currencyCode='EUR'&\$filter=serviceName%20eq%20'Virtual%20Machines'%20and%20armSkuName%20eq%20'Standard_D4s_v5'"
```
```powershell
$filter = "serviceName eq 'Virtual Machines' and armSkuName eq 'Standard_D4s_v5'"
$response = Invoke-RestMethod "https://prices.azure.com/api/retail/prices?currencyCode='EUR'&`$filter=$filter"
$response.Items | Select-Object armSkuName, retailPrice, currencyCode, meterName
```
## Available Filters
| Filter | Example Value | Notes |
| --------------- | -------------------------------- | ------------------------------ |
| `serviceName` | `'Virtual Machines'` | Case-sensitive in preview API |
| `armRegionName` | `'eastus'`, `'westeurope'` | ARM region name |
| `armSkuName` | `'Standard_D4s_v5'` | Full ARM SKU name |
| `priceType` | `'Consumption'`, `'Reservation'` | Pay-as-you-go vs reserved |
| `serviceFamily` | `'Compute'` | Broad category |
| `productName` | `'Virtual Machines Dv5 Series'` | Product line |
| `meterName` | `'D4s v5'`, `'D4s v5 Spot'` | Includes Spot and Low Priority |
> **Warning:** Filter values are **case-sensitive** in API version `2023-01-01-preview` and later.
## Response Fields
| Field | Description |
| ---------------------- | ------------------------------------------------------------------ |
| `armSkuName` | ARM SKU name (e.g., `Standard_D4s_v5`) |
| `retailPrice` | Microsoft retail price (USD unless overridden) |
| `unitOfMeasure` | Usually `1 Hour` for VMs |
| `armRegionName` | Region code |
| `meterName` | Human-readable meter (includes "Spot" / "Low Priority" variants) |
| `productName` | Product line with OS (e.g., "Virtual Machines Dv5 Series Windows") |
| `type` | `Consumption`, `Reservation`, or `DevTestConsumption` |
| `reservationTerm` | `1 Year` or `3 Years` (reservation only) |
| `savingsPlan` | Array with `term` and `unitPrice` (preview API only) |
| `isPrimaryMeterRegion` | Filter to `true` to avoid duplicate regional meters |
## Pagination
API returns max 1,000 records per request. Follow `NextPageLink` in the response to get more:
```json
{ "NextPageLink": "https://prices.azure.com:443/api/retail/prices?$filter=...&$skip=1000" }
```
## Tips for Recommendations
1. **Filter Linux vs Windows**: `productName` contains the OS β e.g., `'Virtual Machines Dv5 Series'` (Linux) vs `'Virtual Machines Dv5 Series Windows'`
2. **Use `isPrimaryMeterRegion eq true`** to deduplicate
3. **Compare Consumption + Reservation + Savings Plan** for full cost picture
4. **Monthly estimate**: `retailPrice Γ 730` (hours/month)
5. **Spot pricing**: Filter `meterName` containing `'Spot'` for discounted interruptible VMs
vm-families.md 5.7 KB
# VM Family Guide
Select a VM family by matching the user's workload to the right category. Families describe hardware intent β not individual SKUs.
> **Source**: [Azure VM sizes overview](https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/overview)
>
> **Note:** This reference may become stale. Before making final recommendations, verify critical specifications (especially Spot VM support, newer series availability, and specific family capabilities) by fetching the relevant learn.microsoft.com documentation.
## Family Selection Table
| Workload | Family | Series | Quota Resource Name | Why |
| ------------------------------------ | --------------------- | ---------------------------------- | ----------------------------------------- | ----------------------------------------------------- |
| Web servers, dev/test, microservices | **General Purpose** | D-series (Dsv5, Ddsv5, Dasv5) | `standardDSv5Family` / `standardDDSv5Family` | Balanced CPU:memory ratio |
| Burstable / intermittent loads | **General Purpose** | B-series (Bsv2, Basv2) | `standardBsv2Family` / `standardBasv2Family` | Low baseline CPU, credits for bursts; cheapest option |
| CI/CD, batch, gaming servers | **Compute Optimized** | F-series (Fsv2, Fasv6) | `standardFSv2Family` | High CPU:memory ratio |
| Relational DBs, in-memory caches | **Memory Optimized** | E-series (Esv5, Edsv5, Easv5) | `standardESv5Family` / `standardEDSv5Family` | High memory:CPU ratio |
| SAP HANA, very large DBs | **Memory Optimized** | M-series (Msv3, Mdsv3) | `standardMSMediumMemoryv3Family` | Extreme memory (up to 4 TB) |
| Big Data, NoSQL, data warehousing | **Storage Optimized** | L-series (Lsv3, Lasv3) | `standardLSv3Family` | High disk throughput and IOPS |
| ML training, inference, rendering | **GPU** | NC-series (NCadsH100v5, NCasT4v3) | `StandardNCadsH100v5Family` | NVIDIA GPU compute |
| Large-scale AI/ML training | **GPU** | ND-series (ND_MI300X_v5, NDH100v5) | `standardNDSH100v5Family` | Multi-GPU, high memory |
| Virtual desktop, cloud gaming | **GPU** | NV-series (NVadsA10v5) | `StandardNVADSA10v5Family` | GPU graphics/visualization |
| Cloud gaming, VDI (AMD GPU) | **GPU** | NG-series (NGadsV620v1) | `StandardNGADSV620v1Family` | AMD Radeon GPU; cost-effective graphics |
| Confidential workloads | **Confidential** | DC-series (DCasv5, DCadsv5) | `standardDCASv5Family` | Hardware-based TEE isolation |
| Confidential + encrypted memory | **Confidential** | EC-series (ECasv5, ECadsv5) | `standardECASv5Family` | TEE isolation with memory encryption |
| CFD, weather simulation, FEA | **HPC** | HB/HC-series (HBv4, HBv5) | `standardHBv4Family` / `standardHBv5Family` | InfiniBand, high memory bandwidth |
| EDA, large memory HPC | **HPC** | HX-series | `standardHXFamily` | Very large memory capacity |
> β οΈ **Do not normalize quota name casing.** The mixed casing (e.g., `standard` vs `Standard`) matches the exact values returned by `az vm list-usage`. Changing them will break quota lookups.
## Decision Tree
```text
Workload needs GPU?
ββ Yes β training/inference? β NC/ND-series
β visualization/VDI? β NV/NG-series
ββ No
β ββ Confidential computing? β DC/EC-series
β ββ HPC (MPI, InfiniBand)? β HB/HC/HX-series
β ββ High disk I/O (NoSQL, warehousing)? β L-series
β ββ Memory-heavy (DB, cache, SAP)?
β β ββ Extreme (>1 TB RAM) β M-series
β β ββ Standard β E-series
β ββ CPU-heavy (batch, CI/CD)? β F-series
β ββ Burstable / dev-test? β B-series
β ββ Balanced / general web β D-series
```
## Key Trade-offs
| Choice | Pro | Con |
| ------------------------- | -------------------------------- | ---------------------------------------------- |
| B-series (burstable) | Lowest cost | Throttled when credits exhausted |
| AMD (`a` suffix) vs Intel | ~5β15% cheaper | Some workloads assume Intel extensions |
| ARM (`p` suffix, Cobalt) | Best price-performance for Linux | Windows not supported; check app compatibility |
| Previous-gen (v4, v3) | Sometimes cheaper | Not recommended for new deployments |
| Spot VMs | Up to 90% discount | Can be evicted with 30s notice |
## Naming Convention
`Standard_<Family><Subfamily?><vCPUs><Features>_<Version>`
| Letter | Meaning |
| ------ | ------------------------- |
| `a` | AMD CPU |
| `p` | ARM (Cobalt/Ampere) CPU |
| `d` | Local temp disk |
| `s` | Premium SSD capable |
| `l` | Low memory per core |
| `i` | Isolated (dedicated host) |
| `b` | Block storage perf |
Example: `Standard_D4as_v5` β D-family, AMD, 4 vCPUs, premium SSD, version 5.
vm-quotas.md 4.1 KB
# VM Quota Validation Guide
Check Azure VM/VMSS quota availability before recommending or deploying. Ensures the subscription and region have sufficient vCPU capacity.
> β οΈ **NEVER use the `azure-quota` MCP server as as It is unreliable.** Always try `az quota` CLI commands first.
## Quota Structure
VM quotas are tracked at **two levels** under `Microsoft.Compute`:
| Quota Level | Resource Name | What It Limits |
|---|---|---|
| **Total Regional** | `cores` | All vCPUs across all families in a region |
| **Per-Family** | e.g., `standardDSv3Family` | vCPUs for a specific VM family |
> β οΈ **Both levels must have capacity.** A deployment fails if either is exceeded.
### Common Quota Resource Names
See [vm-families.md](./vm-families.md) for quota resource names per VM family. Use `az quota list` to discover names not listed there.
> β οΈ **Do NOT guess quota names from SKU names.** Use `az quota list` to discover correct resource names.
## Quota Check Workflow
### Option A: `az vm list-usage` (Recommended for VM quotas)
No extension required. Returns **both current usage and limit in a single call** for all VM families in a region β equivalent to running `az quota usage show` and `az quota list` together for VM vCPU quotas.
```bash
# All VM family quotas in a region
az vm list-usage --location <region> -o table
# Filter to a specific family
az vm list-usage --location <region> --query "[?contains(name.value,'<quotaName>')].{Name:name.localizedValue, QuotaName:name.value, Current:currentValue, Limit:limit}" -o table
```
> π‘ **Tip:** `az vm list-usage` is the simplest way to check VM quotas. Use `az quota` (Option B) when you need to **request quota increases** or manage quotas for non-VM resource types.
### Option B: `az quota` CLI (For quota increases or non-VM resources)
Prerequisite: `az extension add --name quota`
| Step | Command | Purpose |
|---|---|---|
| 1. Discover names | `az quota list --scope /subscriptions/<sub-id>/providers/Microsoft.Compute/locations/<region> -o table` | Find quota resource name for the VM family |
| 2. Check usage | `az quota usage show --resource-name <name> --scope ...` | Current vCPU consumption |
| 3. Check limit | `az quota show --resource-name <name> --scope ...` | Maximum allowed vCPUs |
| 4. Check regional | Repeat steps 2β3 with `--resource-name cores` | Total regional vCPU cap |
### Calculate Capacity
```text
Available = Limit - Current Usage (check both family AND regional)
vCPUs Needed = vCPUs per VM Γ Instance Count
β
Deploy if: vCPUs Needed β€ min(Family Available, Regional Available)
β Blocked if: either is exceeded
```
**Example:** 3Γ `Standard_D4s_v5` (4 vCPUs each) = 12 needed. Family: 100β40 = 60 β
. Regional: 350β280 = 70 β
.
## Handling Insufficient Quota
| Option | Action |
|---|---|
| **Request increase** | `az quota update --resource-name <name> --scope ... --limit-object value=<new-limit> --resource-type dedicated`. Most increases auto-approve within minutes. |
| **Try different region** | Run the quota check workflow against alternative regions to find available capacity |
| **Switch VM family** | Recommend an alternative family with quota (e.g., D-series full β Dads v5 AMD variant) |
## VMSS Considerations
For scale sets, validate against **autoscale maximum**: `vCPUs per VM Γ Max Instance Count`.
| Autoscale Setting | vCPUs to Validate |
|---|---|
| Fixed count (5 instances) | vCPUs Γ 5 |
| Autoscale min=2, max=10 | vCPUs Γ 10 |
## Error Reference
| Error | Cause | Action |
|---|---|---|
| `QuotaExceeded` | Family vCPU limit reached | Request increase or change family/region |
| `OperationNotAllowed` | Subscription lacks capacity | Request quota increase |
| `cores` limit hit | Regional vCPUs exhausted | Request regional increase |
| CLI commands fail entirely | Auth/extension issue | Use MCP fallback (see below) |
## Related Resources
- Invoke the **azure-quotas** skill for complete quota CLI workflows across all Azure providers
- [VM Family Guide](vm-families.md) β Family-to-workload mapping
- [Azure VM quotas documentation](https://learn.microsoft.com/en-us/azure/virtual-machines/quotas)
vmss-guide.md 6.2 KB
# VMSS Guide
Determine when to recommend a Virtual Machine Scale Set (VMSS) over a single VM, and which VMSS configuration to suggest.
> **Note:** This reference provides quick guidance but may become stale. Always verify VMSS features, limitations, and orchestration mode capabilities by fetching the latest documentation from:
> - https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/overview
> - https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/virtual-machine-scale-sets-autoscale-overview
> - https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/orchestration-modes-api-comparison
## What Is a VM Scale Set?
A VMSS creates and manages a group of load-balanced, identically configured VM instances. Key capabilities:
- **Autoscale** β automatically add/remove instances based on metrics or schedules
- **High availability** β spread instances across fault domains and Availability Zones
- **Load balancing** β integrate with Azure Load Balancer (L4) or Application Gateway (L7)
- **Large scale** β up to 1,000 instances per scale set (marketplace images)
- **No extra cost** β you pay only for the underlying VM instances, storage, and networking
## When to Recommend VMSS vs Single VM
| Scenario | Recommend | Reasoning |
| ----------------------------------------------------- | --------- | ------------------------------------------- |
| Stateless web/API behind a load balancer | VMSS | Homogeneous fleet, autoscale on demand |
| Batch or parallel compute jobs | VMSS | Scale out for jobs, scale to zero when idle |
| Autoscale needed (CPU, queue depth, schedule) | VMSS | Built-in autoscale rules |
| Microservices with identical replicas | VMSS | Consistent config, rolling updates |
| High availability across zones (many instances) | VMSS | Automatic zone distribution |
| Single long-lived server (jumpbox, domain controller) | VM | No scaling benefit; simpler config |
| Unique per-instance configuration | VM | Scale sets assume identical instances |
| Quick proof of concept or dev/test | VM | Faster to stand up, lower complexity |
## Orchestration Modes
VMSS supports two orchestration modes. **Flexible** is recommended for all new workloads.
| Feature | Flexible (recommended) | Uniform (legacy) |
| ------------------------ | ---------------------- | ---------------- |
| Mix VM sizes in one set | β
Yes | β No |
| Add existing VMs to set | β
Yes | β No |
| Availability Zone spread | β
Automatic | β
Automatic |
| Fault domain control | β
Yes | β
Yes |
| Max instances | 1,000 | 1,000 |
| Spot instances | β
Yes | β
Yes |
| Single-instance VMSS | β
Yes | β No |
| VM model updates | Automatic, Manual, Rolling | Automatic, Manual, Rolling |
> **Warning:** Orchestration mode cannot be changed after creation. Always recommend Flexible unless the user has a specific Uniform requirement.
## Autoscale Patterns
| Pattern | Trigger | Example |
| ------------------ | ---------------------------------------- | ------------------------------------------------------------ |
| **Metric-based** | CPU, memory, queue length, custom metric | Scale out when avg CPU > 70% for 5 min |
| **Schedule-based** | Time of day, day of week | Scale to 10 instances MonβFri 8 AM; scale down to 2 at night |
| **Combined** | Metric + schedule together | Baseline schedule with metric burst capacity |
| **Predictive** | ML-forecasted demand (preview) | Pre-scale before expected traffic spike |
### Autoscale Best Practices
- Set a **minimum instance count β₯ 2** for production HA
- Use a **cool-down period** (default 5 min) to avoid flapping
- Scale out aggressively, scale in conservatively (asymmetric rules)
- Monitor with [Azure Monitor autoscale diagnostics](https://learn.microsoft.com/en-us/azure/azure-monitor/autoscale/autoscale-best-practices)
## Networking
| Component | When to Use |
| ----------------------- | ------------------------------------------------------------------------ |
| **Azure Load Balancer** | Layer-4 (TCP/UDP) traffic distribution; most common for backend services |
| **Application Gateway** | Layer-7 (HTTP/HTTPS) with TLS termination, URL routing, WAF |
| **No load balancer** | Batch/HPC jobs where instances pull work from a queue |
## Cost Estimation Tips
- VMSS itself is **free** β cost is the sum of per-instance VM pricing
- Estimate at **min** and **max** instance counts for autoscale budgets
- Use **Spot instances** in VMSS for up to 90% savings on interruptible workloads
- Combine with **Reservations** or **Savings Plans** on the baseline instance count
## Key VMSS Limits
| Limit | Value |
| -------------------------------------- | ---------------------------------- |
| Max instances per scale set | 1,000 (marketplace/gallery images) |
| Max instances (managed image) | 600 |
| Scale sets per subscription per region | 2,500 |
| Scale operations concurrency | Up to 1,000 VMs in a single batch |
## Further Reading
- [VMSS orchestration modes](https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/virtual-machine-scale-sets-orchestration-modes)
- [Autoscale best practices](https://learn.microsoft.com/en-us/azure/azure-monitor/autoscale/autoscale-best-practices)
- [VMSS networking](https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/virtual-machine-scale-sets-networking)
- [VMSS Flexible portal quickstart](https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/flexible-virtual-machine-scale-sets-portal)
capacity-reservation.md 7.8 KB
# Azure Capacity Reservation
Helps users create and configure Azure Capacity Reservation Groups (CRGs) to guarantee VM compute capacity in a specific region without deploying VMs.
## Reference Files
Read these before responding to the user:
| Signal | Reference |
|--------------------------------------------------|------------------------------------------------------------------------------|
| General CRG concepts, CLI commands, finding CRGs | [Capacity Reservation Overview](references/capacity-reservation-overview.md) |
| Associate/disassociate VM or VMSS with a CRG | [Association & Disassociation](references/association-disassociation.md) |
## When to Use This Workflow
Activate this workflow when the user explicitly asks about Capacity Reservation Groups (CRGs) or capacity reservations.
Also **proactively suggest** CRG when the user's scenario matches any of these patterns:
- **Deployment failure is unacceptable** β disaster recovery, customer-facing services, or mission-critical workloads where capacity unavailability would cause an outage
- **Known scale-out events** β product launches, seasonal traffic spikes, or planned migrations where capacity must be guaranteed ahead of time
- **In-demand SKUs** β GPU, high-memory, or new/popular VM sizes that are frequently capacity-constrained
- **Specific SKU + zone + region required** β the workload cannot fall back to a different size, zone, or region
- **Centralized capacity pooling** β capacity is being managed centrally across multiple subscriptions (CRGs support cross-subscription sharing)
> **Note:** CRGs are typically used for critical workloads only, not all deployments. They are SLA-backed but billed at pay-as-you-go rates whether capacity is consumed or not.
## Key Concepts
| Concept | Description |
|-----------------------------------|------------------------------------------------------------------------------------------------------------------|
| **Capacity Reservation Group** | A logical container that holds one or more capacity reservations; must be associated with VMs at deployment time |
| **Capacity Reservation** | A reservation for a specific VM size and quantity in a specific Availability Zone |
| **Scope** | CRGs are scoped to a single Azure region and subscription |
| **Billing** | Charges begin as soon as the reservation is created, whether or not VMs are deployed against it |
## Workflow
### Step 1: Gather Requirements
Ask the user for (infer when possible, except where noted):
| Requirement | Required | Notes |
|--------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Region** | Yes | Infer from context if possible (e.g., eastus, westeurope) |
| **VM size(s)** | Yes | e.g., Standard_D4s_v5, Standard_E8s_v5 |
| **Quantity** | Yes | **Always ask β do not infer.** |
| **Availability Zone(s)** | No | CRGs can be created without zones. Only include zones if the user explicitly requests a zonal reservation. **Do not pick a zone on the user's behalf** unless they explicitly ask for any/random zone |
| **Resource group** | Yes | Existing or new resource group name |
### Step 2: Create Capacity Reservation Group and Reservation
> β οΈ **PowerShell users:** Replace `\` line continuations with backticks (`` ` ``) or collapse commands to a single line.
```bash
# Create the CRG
# Zonal (specify one or more zones the group will support):
az capacity reservation group create \
-g <resource-group> \
-n <crg-name> \
-l <region> \
--zones 1 2 3
# Non-zonal (omit --zones for regional-only reservations):
az capacity reservation group create \
-g <resource-group> \
-n <crg-name> \
-l <region>
# Create the reservation
# If the CRG is zonal, specify --zone matching one of the group's zones.
# If the CRG is non-zonal, omit --zone.
az capacity reservation create \
-g <resource-group> \
-c <crg-name> \
-n <reservation-name> \
--sku <vm-size> \
--capacity <quantity> \
--zone <zone> # omit if CRG is non-zonal
```
### Step 3: Verify Reservation
```bash
az capacity reservation show \
-g <resource-group> \
-c <crg-name> \
-n <reservation-name> \
--query "{name:name, sku:sku, capacity:sku.capacity, provisioningState:provisioningState}"
```
### Step 4: Offer Next Steps
- Associate VMs or VMSS with the Capacity Reservation Group at deployment time
- See [Capacity Reservation Overview](references/capacity-reservation-overview.md) for detailed guidance
## Managing Existing Reservations
For operations beyond creation, see the relevant section in the [Capacity Reservation Overview](references/capacity-reservation-overview.md):
- **Associate a VM or VMSS** with a CRG β see [Association & Disassociation](references/association-disassociation.md)
- **Disassociate a VM or VMSS** from a CRG β see [Association & Disassociation](references/association-disassociation.md)
- **Find a matching CRG** for a VM, or enumerate all reservations/groups β see [Finding Valid CRGs](references/capacity-reservation-overview.md#finding-valid-crgs-for-a-vm)
## Error Handling
| Scenario | Action |
|--------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| SKU not available in region/zone | Run `az vm list-skus --location <region> --size <vm-size> --resource-type virtualMachines -o table`. Suggest alternatives from output |
| Quota exceeded | Use the **azure-quotas** skill to check usage and request an increase |
| Insufficient platform capacity | Azure lacks physical hardware in the region/zone. Suggest a different zone, region, or VM size |
| Duplicate SKU + zone in CRG | Only one reservation per VM size per zone (or per size if non-zonal) is allowed in a CRG. Update the existing reservation's capacity instead |
association-disassociation.md 3.8 KB
# Associating and Disassociating VMs/VMSS with a Capacity Reservation Group
## Association Model
```text
Capacity Reservation Group (CRG)
βββ Capacity Reservation: Standard_D4s_v5 Γ 5 (Zone 1)
βββ Capacity Reservation: Standard_D4s_v5 Γ 3 (Zone 2)
βββ Capacity Reservation: Standard_E8s_v5 Γ 2 (Zone 1)
VM / VMSS
βββ capacityReservationGroup.id = <CRG resource ID>
βββ Azure auto-matches to a reservation with the right VM size + zone
```
### Associating VMs
Set the `capacityReservationGroup` property when creating or updating a VM.
#### New VM
```bash
az vm create \
-g <rg> \
-n <vm-name> \
--image <image> \
--size Standard_D4s_v5 \
--zone 1 \
--capacity-reservation-group <crg-id>
```
#### Existing VM
Zonal VMs can be associated while running:
```bash
az vm update -g <rg> -n <vm-name> --capacity-reservation-group <crg-id>
```
Regional VMs (no zone) must be deallocated first:
```bash
az vm deallocate -g <rg> -n <vm-name>
az vm update -g <rg> -n <vm-name> --capacity-reservation-group <crg-id>
az vm start -g <rg> -n <vm-name>
```
### Associating VMSS
```bash
az vmss create \
-g <rg> \
-n <vmss-name> \
--image <image> \
--vm-sku Standard_D4s_v5 \
--instance-count 5 \
--zones 1 \
--capacity-reservation-group <crg-id>
```
Existing VMSS can be associated using `az vmss update` similarly to VMs. Regional VMSS must be deallocated first. Zonal VMSS can be associated without deallocating, but this is currently a [Preview feature](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-associate-virtual-machine-scale-set).
## Disassociating from a Capacity Reservation Group
Both the VM/VMSS and the underlying capacity reservation logically occupy capacity. Azure imposes constraints to avoid ambiguous allocation states, so you cannot simply remove the association while resources are running against it.
There are three ways to disassociate. The commands below use `az vm` β for VMSS, substitute `az vmss` and add `az vmss update-instances --instance-ids "*"` as a final step when using a **Manual** upgrade policy.
### Option 1: Deallocate, then remove association
Best when the VM/VMSS can tolerate downtime.
```bash
az vm deallocate -g <rg> -n <vm-name>
az vm update -g <rg> -n <vm-name> --capacity-reservation-group None
az vm start -g <rg> -n <vm-name> # optional
```
### Option 2: Set reserved quantity to zero, then remove association
Best when the VM/VMSS cannot be deallocated and the reservation is no longer needed.
```bash
az capacity reservation update \
-g <rg> --capacity-reservation-group <crg> \
-n <reservation-name> --capacity 0
az vm update -g <rg> -n <vm-name> --capacity-reservation-group None
```
### Option 3: Delete the VM/VMSS
Deleting the resource automatically removes the association. Some latency may occur before the capacity reservation allocation state updates.
### VMSS Upgrade Policy Behavior
| Policy | Behavior |
|---------------|------------------------------------------------------------------------|
| **Automatic** | Instances update automatically β no further action needed |
| **Rolling** | Instances update in batches with an optional pause between them |
| **Manual** | You must run `az vmss update-instances --instance-ids "*"` per update |
## Learn More
- [Associate a VM to a Capacity Reservation Group](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-associate-vm)
- [Remove/disassociate a VM from a Capacity Reservation Group](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-remove-vm)
- [Remove/disassociate a VMSS from a Capacity Reservation Group](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-remove-virtual-machine-scale-set)
capacity-reservation-overview.md 5.9 KB
# Capacity Reservation Overview
Reference material for Azure Capacity Reservation Groups and Capacity Reservations.
## What Is a Capacity Reservation Group?
A Capacity Reservation Group (CRG) is a logical container for one or more capacity reservations. It acts as the association point for VMs and VMSS β you associate a VM or scale set with the **group**, and Azure matches the VM to a suitable reservation within that group.
## Constraints
| Constraint | Detail |
|--------------------------------|------------------------------------------------------------------------------------------------------------|
| **Region-scoped** | A CRG and all its reservations must be in the same Azure region |
| **Zone-specific** | Each reservation targets a specific Availability Zone (or is non-zonal) |
| **Subscription-scoped** | A CRG lives in a single subscription but can be shared with other subscriptions via the `sharing` property |
| **VM size per reservation** | Each capacity reservation covers exactly one VM size |
| **Billing starts immediately** | You are charged for reserved capacity whether or not VMs are running against it |
## Association and Disassociation
See [association-disassociation.md](association-disassociation.md) for how to associate and disassociate VMs/VMSS with a CRG.
## Common CLI Commands
| Action | Command |
|-----------------------------|-------------------------------------------------------------------------------------------------------------|
| List CRGs | `az capacity reservation group list` |
| Show CRG | `az capacity reservation group show -g <rg> -n <crg> --instance-view` |
| Delete CRG | `az capacity reservation group delete -g <rg> -n <crg>` |
| List reservations | `az capacity reservation list -g <rg> --capacity-reservation-group <crg>` |
| Update reservation quantity | `az capacity reservation update -g <rg> --capacity-reservation-group <crg> -n <res> --capacity <new-count>` |
| Delete reservation | `az capacity reservation delete -g <rg> --capacity-reservation-group <crg> -n <res>` |
## Finding Valid CRGs for a VM
To associate a VM with a CRG, the CRG must contain a capacity reservation that matches the VM's **size**, **region**, and **zone** (if zonal). While `az capacity reservation group list` can enumerate CRGs at the subscription level, filtering down to matching reservations across many groups is inefficient. Azure Resource Graph is recommended for cross-resource-group discovery.
### Option 1: Azure Resource Graph (recommended)
ARG can query all capacity reservations across resource groups in a single call, filtering by location, VM size, and zone. This is the most efficient approach.
> β οΈ **Prerequisite:** `az extension add --name resource-graph`
You must collapse this query to a **single line** before running it:
```bash
az graph query -q "
Resources
| where type =~ 'Microsoft.Compute/capacityReservationGroups/capacityReservations'
| where location =~ '<region>'
| where properties.provisioningState =~ 'Succeeded'
| where sku.name =~ '<vm-size>'
| project id,
crgId = extract('(.*)/capacityReservations', 1, id),
resourceGroup,
zones,
size = sku.name,
capacity = coalesce(sku.capacity, 0),
associationCount = coalesce(array_length(properties.virtualMachinesAssociated), 0),
location
" --query "data[]" -o table
```
The `crgId` in the output is the parent Capacity Reservation Group resource ID β this is the value to use when associating a VM or VMSS.
To further narrow results for zonal VMs, add a zone filter:
```kql
| where zones has '<zone>'
```
### Option 2: CLI enumeration
If ARG is unavailable, list CRGs per resource group and inspect their reservations:
```bash
# List all CRGs
az capacity reservation group list -o table
# List reservations within a CRG and check for matching size/capacity
az capacity reservation list \
-g <rg> \
--capacity-reservation-group <crg-name> \
--query "[?sku.name=='<vm-size>'].{name:name, size:sku.name, capacity:sku.capacity, zones:zones}" \
-o table
```
## Estimating Reservation Cost
Capacity reservations are billed at the same pay-as-you-go rate as the underlying VM size, whether or not VMs are running against them. Use the [Retail Prices API guide](../../../references/retail-prices-api.md) (unauthenticated) to look up hourly rates.
**Estimated monthly cost:** `quantity Γ hourly rate Γ 730`
> β οΈ Prices returned are **estimates based on current retail pay-as-you-go rates**, not a final cost or contractual commitment. Actual charges may vary due to taxes, discounts (Reserved Instances, Savings Plans), or price changes.
## Important Notes
- **Deletion is blocked until prerequisites are met:** Azure rejects a CRG delete unless all VMs/VMSS are disassociated and all capacity reservations are deleted. Order: disassociate VMs/VMSS β delete reservations β delete group.
- **Quota required:** Capacity reservations consume vCPU quota just like running VMs.
## Learn More
- [Azure Capacity Reservations documentation](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-overview)
- [Create a Capacity Reservation](https://learn.microsoft.com/en-us/azure/virtual-machines/capacity-reservation-create)
- [Association and disassociation](association-disassociation.md)
essential-machine-management.md 4.9 KB
# Essential Machine Management (EMM) Workflow
Routes EMM-related requests to the appropriate reference based on user intent.
## Overview
Essential Machine Management simplifies onboarding and configuration of management for Azure VMs and Arc-enabled servers at the subscription level. When enabled, all VMs in a subscription are automatically enrolled with a curated set of monitoring, security, and operations features.
> β οΈ **Warning:** EMM is currently in **public preview**.
## Routing
```text
User intent?
ββ Enable / onboard / enroll subscription for EMM
β ββ Copilot-guided (default) β Load [EMM Enable Flow](references/emm-enable-flow.md)
β
ββ User explicitly asks for portal guidance
β ββ Load [EMM Enable Flow (Portal)](references/emm-enable-flow-portal-guidance.md)
β
ββ What is EMM / features / pricing / tiers
β ββ Load [EMM Overview](references/emm-overview.md)
β
ββ Prerequisites / permissions / roles / managed identity
β ββ Load [EMM Prerequisites](references/emm-prerequisites.md)
β
ββ View enrolled subscriptions / browse / status
β ββ See "Browse Enrolled Subscriptions" below
β
ββ Offboard / disable EMM for a subscription
β ββ See "Offboard a Subscription" below
β
ββ Troubleshoot EMM issues
ββ See "Troubleshooting" below
```
| Signal | Reference |
| ------ | --------- |
| "enable EMM", "onboard subscription", "enroll VMs", "set up machine management" | [EMM Enable Flow](references/emm-enable-flow.md) |
| User explicitly mentions "portal", "Azure portal", "portal UI" | [EMM Enable Flow (Portal)](references/emm-enable-flow-portal-guidance.md) |
| "what is EMM", "features", "pricing", "tiers", "what does EMM include" | [EMM Overview](references/emm-overview.md) |
| "permissions", "roles", "prerequisites", "managed identity for EMM" | [EMM Prerequisites](references/emm-prerequisites.md) |
> β οΈ **Important:** Only route to the portal guide when the user explicitly mentions "portal". All other enable requests use the Copilot-guided flow.
## Browse Enrolled Subscriptions
Query the EMM resource on each subscription to check enrollment status:
```text
GET https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.ManagedOps/managedOps/default?api-version=2025-07-28-preview
```
| Response | Meaning |
| -------- | ------- |
| `200` with `provisioningState: Succeeded` | Subscription is enrolled |
| `200` with `provisioningState: Failed` | Enrollment attempted but failed β check error details |
| `404` | Subscription is not enrolled |
When enrolled, the response includes:
- **SKU/tier** β e.g. Essential
- **Enabled services** β Azure Monitor Insights, Update Manager, Change Tracking, Policy & Machine Configuration, Defender CSPM, Defender for Servers
- **UAMI** β the user-assigned managed identity resource ID
- **Workspaces** β Log Analytics and Azure Monitor workspace resource IDs
- **Created by / date** β who enrolled and when (in `systemData`)
To scan multiple subscriptions, use `mcp_azure_mcp_subscription_list` to list available subscriptions, then query each one. Report results as a table:
```text
| Subscription | Status | SKU | Services Enabled |
```
## Offboard a Subscription
To disable EMM for a subscription, follow the "Disable EMM (Offboard)" section in [EMM Enable Flow](references/emm-enable-flow.md).
> β οΈ **Warning:** When you disable a subscription, machines no longer use consolidated pricing. Pricing reverts to standard per-service pricing which may increase costs. Existing VM configurations are not removed β disable unneeded services manually.
## Troubleshooting
For common EMM issues, refer to the official documentation:
- [Troubleshoot Essential Machine Management (Preview)](https://learn.microsoft.com/en-us/azure/operations/configuration-enrollment-troubleshoot)
Common issues include:
- Missing role assignments (EMM Administrator, Managed Identity Operator, Resource Policy Contributor)
- Resource provider `Microsoft.ManagedOps` not registered in the subscription
- UAMI lacking Contributor permission on the subscription
- Cross-subscription workspace access requires additional RP registration
## Error Handling
| Error | Cause | Remediation |
| ----- | ----- | ----------- |
| Permission denied during enable | User lacks required roles | Assign EMM Administrator, Managed Identity Operator, and Resource Policy Contributor roles |
| UAMI role check fails | Managed identity lacks Contributor | Assign Contributor role to the UAMI at subscription scope |
| RP not registered | `Microsoft.ManagedOps` not registered | Register via `Register-AzResourceProvider -ProviderNamespace "Microsoft.ManagedOps"` |
| Cross-subscription workspace error | Workspace in different sub without RP registration | Register `Microsoft.ManagedOps` in the workspace subscription and assign EMM Administrator on the workspace resource group |
| Deployment fails | ARM template validation error | Check deployment link in browse view for detailed error; verify all prerequisites |
emm-enable-flow-portal-guidance.md 3.1 KB
# EMM Enable Flow (Portal)
Step-by-step guide for enabling Essential Machine Management through the Azure portal UI.
## Quick Reference
| Property | Value |
| -------- | ----- |
| Portal blade | `EnableMachineManagement.ReactView` |
| Extension | `Microsoft_Azure_Computehub` |
| Portal path | Compute infrastructure β Monitoring+Operations β Essential Machine Management β Enable |
| Resource type | `Microsoft.ManagedOps/ManagedOps` |
## Enable Flow Steps
The portal enable flow is a multi-tab wizard with 4 tabs:
### Tab 1: Scope
Select the target subscription and managed identity.
| Field | Description | Required |
| ----- | ----------- | -------- |
| Subscription | The subscription to enable EMM for. Shows VM and Arc machine counts per subscription. | β
|
| User-assigned managed identity | UAMI with Contributor on the subscription. Used for onboarding VMs. | β
|
**Validation displayed:**
- Required user role assignments vs current user role assignments
- Required UAMI role assignments vs current UAMI role assignments
> π‘ **Tip:** If roles are missing, the UI shows exactly which roles are needed. Assign them before proceeding.
### Tab 2: Configure
Select or create the monitoring workspaces.
| Field | Description | Required |
| ----- | ----------- | -------- |
| Log Analytics workspace | Collects log data (Change Tracking & Inventory). Can create new inline. | β
|
| Azure Monitor workspace | Collects metrics data (VM Insights). Can create new inline. | β
|
**Notes:**
- Workspaces can be in a different subscription than the one being enabled
- If cross-subscription, additional RP registration and role assignments are needed (see [Prerequisites](emm-prerequisites.md))
### Tab 3: Security
Optional security add-ons.
| Feature | Description | Cost |
| ------- | ----------- | ---- |
| Foundational CSPM | Agentless, risk-prioritized cloud security posture insights. Always included. | Free |
| Defender CSPM | Advanced CSPM with attack path analysis. Optional toggle. | Paid |
| Defender for Cloud | Comprehensive server protection with EDR, vulnerability management, file integrity monitoring. Optional toggle. | Paid |
### Tab 4: Review & Enable
Displays a summary of all selections:
- Included features (always: Azure Monitor VM Insights, Azure Policy & Machine Configurations, Change Tracking & Inventory, Azure Update Manager)
- Selected scope (subscription, UAMI)
- Configure selections (Log Analytics workspace, Azure Monitor workspace)
- Security add-ons enabled
- Pricing information with links
Clicking **Enable** triggers:
1. Resource provider registrations on the target subscription
2. Cross-subscription RP registration if workspaces are in a different subscription
3. Subscription-level ARM template deployment
## What Happens After Enable
- A deployment is created: `ManagedOps_{uamiName}_{subscriptionId}`
- Policy assignments are created to configure all VMs in the subscription
- Remediation tasks are created for existing VMs
- New VMs added to the subscription are automatically enrolled
- The subscription appears in the browse view with status "Succeeded"
emm-enable-flow.md 9.2 KB
# EMM Enable Flow
Copilot-guided step-by-step workflow for enabling Essential Machine Management on a subscription. Copilot orchestrates each step, triggering the necessary CLI commands or API calls on behalf of the user.
## Quick Reference
| Property | Value |
| -------- | ----- |
| Resource type | `Microsoft.ManagedOps/ManagedOps` |
| Resource provider | `Microsoft.ManagedOps` |
| API version | `2025-07-28-preview` |
| Deployment scope | Subscription-level |
## Workflow Steps
### Step 1: Select Target Subscription
Ask the user which subscription to enable EMM for. Use MCP tools to list subscriptions if needed.
| MCP Tool | Purpose |
| -------- | ------- |
| `mcp_azure_mcp_subscription_list` | List available subscriptions |
Store the selected `subscriptionId` and `tenant` for all subsequent steps.
### Step 2: Validate User Role Assignments
Check that the current user has the 3 required roles on the target subscription. This requires two API calls: one to get the user's role assignments, and one to get all role definitions. Then compare the user's assigned permissions against the required roles.
**Step 2a: Get current user's object ID**
```bash
az rest --method GET --url "https://graph.microsoft.com/v1.0/me" --query id -o tsv
```
**Step 2b: Get user's role assignments on the subscription**
```text
GET https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&$filter=assignedTo('{objectId}')
```
> π‘ **Tip:** The `assignedTo` filter is self-scoped β it allows the user to query their own role assignments without needing `Microsoft.Authorization/roleAssignments/read`. However, a 403 will still occur if the user has no role on the subscription at all.
**Step 2c: Get all role definitions on the subscription**
```text
GET https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-04-01
```
**Step 2d: Join and check permissions**
For each role assignment, match `properties.roleDefinitionId` to the role definitions to resolve the role name and its `properties.permissions[]`. Then check whether the user's combined permissions cover all three required roles:
| Required Role | Key Permissions (actions) |
| ------------- | ------------------------ |
| Essential Machine Management Administrator | `Microsoft.ManagedOps/managedOps/*`, `Microsoft.Insights/dataCollectionRules/*`, `Microsoft.Monitor/accounts/*`, `Microsoft.OperationalInsights/workspaces/read`, `Microsoft.Security/pricings/*` |
| Managed Identity Operator | `Microsoft.ManagedIdentity/userAssignedIdentities/*/read`, `Microsoft.ManagedIdentity/userAssignedIdentities/*/assign/action` |
| Resource Policy Contributor | `Microsoft.Authorization/policyassignments/*`, `Microsoft.Authorization/policydefinitions/*`, `Microsoft.PolicyInsights/*` |
> π‘ **Tip:** If the user has **Owner** at subscription scope, they satisfy all required permissions. Check for these first as a fast path.
```text
Check result?
ββ All 3 roles covered β Proceed to Step 3
ββ Owner found β All roles satisfied, proceed to Step 3
ββ Missing roles β Inform user which roles are missing and how to assign them, then re-check
```
### Step 3: Select or Create a User-Assigned Managed Identity (UAMI)
Ask the user to provide an existing UAMI or create a new one. The UAMI must have **Contributor** on the target subscription.
Verify the UAMI's role using the same API pattern as Step 2, but filter by the UAMI's principal ID (object ID) instead of the user's:
```text
GET https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&$filter=assignedTo('{uamiPrincipalId}')
```
Check that at least one assignment resolves to the **Contributor** role definition.
> π‘ **Tip:** If the UAMI lacks the Contributor role, guide the user to assign it before proceeding.
Store the full UAMI resource ID: `/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.ManagedIdentity/userAssignedIdentities/<name>`
### Step 4: Select or Create Monitoring Workspaces
Ask the user for a **Log Analytics workspace** and an **Azure Monitor workspace**. Offer to create new ones if needed.
| Resource | CLI Command | Purpose |
| -------- | ----------- | ------- |
| Log Analytics workspace (list) | `az monitor log-analytics workspace list --subscription <subId> -o table` | List existing workspaces |
| Log Analytics workspace (create) | `az monitor log-analytics workspace create --workspace-name <name> --resource-group <rg> --subscription <subId> --location <location>` | Create new workspace |
| Azure Monitor workspace (list) | `az resource list --resource-type "Microsoft.Monitor/accounts" --subscription <subId> -o table` | List existing workspaces |
| Azure Monitor workspace (create) | `az resource create --resource-type "Microsoft.Monitor/accounts" --name <name> --resource-group <rg> --subscription <subId> --location <location> --properties "{}"` | Create new workspace |
> β οΈ **Warning:** If workspaces are in a **different subscription** than the target:
> - Register `Microsoft.ManagedOps` RP in the workspace subscription
> - User needs **EMM Administrator** role on the workspace resource group
> - UAMI needs **Contributor** on the workspace resource group
Store both workspace resource IDs.
### Step 5: Configure Security Options
Ask the user about optional security add-ons.
| Feature | Default | Cost |
| ------- | ------- | ---- |
| Foundational CSPM | Always enabled | Free |
| Defender CSPM | Disabled | Paid |
| Defender for Cloud | Disabled | Paid |
Store user selections as `enabled` or `disabled`.
### Step 6: Register Resource Providers
Register required RPs on the target subscription before deployment.
```bash
az provider register --namespace Microsoft.ManagedOps --subscription <subscriptionId>
az provider register --namespace Microsoft.OperationsManagement --subscription <subscriptionId>
az provider register --namespace Microsoft.PolicyInsights --subscription <subscriptionId>
az provider register --namespace Microsoft.Insights --subscription <subscriptionId>
az provider register --namespace Microsoft.OperationalInsights --subscription <subscriptionId>
az provider register --namespace Microsoft.Monitor --subscription <subscriptionId>
az provider register --namespace Microsoft.ManagedIdentity --subscription <subscriptionId>
az provider register --namespace Microsoft.Security --subscription <subscriptionId>
```
> π‘ **Tip:** RP registration is idempotent β safe to run even if already registered.
### Step 7: Deploy EMM via ARM API
Submit the PUT request to enable EMM on the subscription.
```text
PUT /subscriptions/{subscriptionId}/providers/Microsoft.ManagedOps/managedOps/default?api-version=2025-07-28-preview
```
Request body:
```json
{
"properties": {
"desiredConfiguration": {
"defenderCspm": "<enabled|disabled>",
"defenderForServers": "<enabled|disabled>",
"changeTrackingAndInventory": {
"logAnalyticsWorkspaceId": "<log-analytics-workspace-resource-id>"
},
"userAssignedManagedIdentityId": "<uami-resource-id>",
"azureMonitorInsights": {
"azureMonitorWorkspaceId": "<azure-monitor-workspace-resource-id>"
}
}
}
}
```
Populate the request body with the values collected in previous steps.
### Step 8: Verify Enrollment
After deployment completes, confirm the subscription is enrolled.
```text
GET /subscriptions/{subscriptionId}/providers/Microsoft.ManagedOps/managedOps/default?api-version=2025-07-28-preview
```
```text
Deployment status?
ββ Succeeded β Report success to user. All existing VMs will be enrolled via policy remediation.
ββ In progress β Wait and re-check after a short interval.
ββ Failed β Read error details and route to Error Handling in the parent workflow.
```
## Disable EMM (Offboard)
To disable EMM for a subscription:
```text
DELETE /subscriptions/{subscriptionId}/providers/Microsoft.ManagedOps/managedOps/default?api-version=2025-07-28-preview
```
> β οΈ **Warning:** Disabling reverts pricing to standard per-service rates, which may increase costs. Existing VM configurations are not removed.
## Error Handling
| Error | Cause | Remediation |
| ----- | ----- | ----------- |
| 403 on role check | User has no RBAC role assignment on the subscription (the `assignedTo` filter is self-scoped and does not require `roleAssignments/read`, but the user must have at least one role on the subscription) | Inform user they lack Owner or Contributor role on this subscription and cannot proceed with EMM enrollment |
| Missing required roles | User missing EMM Administrator, Managed Identity Operator, or Resource Policy Contributor | Guide user to assign missing roles, then re-validate |
| UAMI lacks Contributor | Managed identity missing Contributor role | Assign Contributor to the UAMI at subscription scope |
| RP registration failed | Insufficient permissions to register providers | User needs Contributor or Owner on the subscription |
| PUT deployment fails | ARM validation error | Check error details; verify all prerequisites met |
| Cross-subscription error | Workspace in different sub without RP/role setup | Register `Microsoft.ManagedOps` in workspace sub; assign roles on workspace RG |
emm-overview.md 2.4 KB
# EMM Overview
Essential Machine Management (EMM) simplifies onboarding and configuration of management for Azure VMs and Arc-enabled servers at the subscription level.
## What is EMM?
When you enable a subscription for EMM, all VMs and Arc-enabled servers in that subscription are automatically enrolled and configured with a curated set of management features. Any new VMs added to the subscription are also automatically enrolled.
## Features Included
### Essentials Tier (Always Enabled)
| Feature | Description |
| ------- | ----------- |
| Azure Monitor VM Insights | Monitors VM performance and health, configures metric-based recommended alerts |
| Azure Update Manager | Automates OS update deployment |
| Azure Machine Configuration | Audits Azure security baseline policy |
| Change Tracking & Inventory | Tracks VM configuration changes, maintains resource inventory |
### Security Tier (Optional Add-ons)
| Feature | Description | Cost |
| ------- | ----------- | ---- |
| Foundational CSPM | Agentless, risk-prioritized security posture insights | Free |
| Defender CSPM | Advanced CSPM with attack path analysis | Paid |
| Defender for Cloud | EDR, vulnerability management, file integrity monitoring, threat detection | Paid |
## Pricing
- **Azure VMs:** Essentials tier features at no extra charge
- **Arc-enabled servers with Windows Server SA/PayGo/ESU:** No extra charge
- **Other Arc-enabled servers:** $9/server/month once billing is enabled (future date, currently free in preview)
- **Change Tracking & Inventory logs:** Incur separate Log Analytics ingestion charges
- **Security tier add-ons:** Standard Microsoft Defender pricing applies
## Key Characteristics
- **Subscription-level scope:** Enables for all VMs in a subscription at once
- **No VM exclusion:** Currently no ability to exclude individual VMs
- **Existing services preserved:** If a VM already has Update Manager with a maintenance schedule, it keeps that schedule
- **REST API available:** Official docs focus on the portal experience, but a REST API (`Microsoft.ManagedOps`) is available and used by the Copilot-guided flow
- **Resource type:** `Microsoft.ManagedOps/ManagedOps`
## Documentation Links
- [Essential Machine Management (Preview)](https://learn.microsoft.com/en-us/azure/operations/configuration-enrollment)
- [Troubleshoot EMM](https://learn.microsoft.com/en-us/azure/operations/configuration-enrollment-troubleshoot)
emm-prerequisites.md 3.5 KB
# EMM Prerequisites
Requirements that must be met before enabling Essential Machine Management.
## Required Azure Resources
| Resource | Purpose |
| -------- | ------- |
| Log Analytics workspace | Collects log data from Change Tracking & Inventory |
| Azure Monitor workspace | Collects metrics data from VM Insights |
| User-assigned managed identity (UAMI) | Used to onboard and configure VMs in the subscription |
## Required User Roles
The user performing the enrollment must have these roles on the target subscription:
| Role | Description |
| ---- | ----------- |
| Essential Machine Management Administrator | Manages EMM resources, DCRs, monitor/workspace operations, security pricing |
| Managed Identity Operator | Reads and assigns user-assigned identities |
| Resource Policy Contributor | Creates/modifies resource policies, policy assignments, and exemptions |
### Cross-Subscription Workspace Scenario
If the Log Analytics or Azure Monitor workspace is in a **different subscription**:
- The user must also have **Essential Machine Management Administrator** on the resource group of the workspace
- The `Microsoft.ManagedOps` RP must be registered in the workspace subscription
## Required Managed Identity Roles
The user-assigned managed identity must have:
| Role | Scope |
| ---- | ----- |
| Contributor | Target subscription being enabled |
If workspaces are in a different subscription:
- **Contributor** on the resource group of the Log Analytics workspace and/or Azure Monitor workspace
## EMM Administrator Permissions Detail
The Essential Machine Management Administrator role includes these actions:
```text
Microsoft.Resources/deployments/*
Microsoft.Insights/dataCollectionRules/read
Microsoft.Insights/dataCollectionRules/write
Microsoft.Monitor/accounts/write
Microsoft.Monitor/accounts/read
Microsoft.ManagedOps/managedOps/read
Microsoft.ManagedOps/managedOps/write
Microsoft.ManagedOps/managedOps/delete
Microsoft.OperationsManagement/solutions/read
Microsoft.OperationsManagement/solutions/write
Microsoft.OperationalInsights/workspaces/read
Microsoft.OperationalInsights/workspaces/sharedkeys/action
Microsoft.OperationalInsights/workspaces/sharedkeys/read
Microsoft.OperationalInsights/workspaces/listKeys/action
Microsoft.Resources/subscriptions/resourceGroups/read
Microsoft.Insights/metricAlerts/write
Microsoft.Insights/metricAlerts/read
Microsoft.Security/pricings/write
Microsoft.Security/pricings/read
```
## Resource Provider Registrations
The following RPs are registered automatically during the enable flow:
| Resource Provider | Purpose |
| ----------------- | ------- |
| `Microsoft.ManagedOps` | Core EMM resource provider |
| `Microsoft.OperationsManagement` | Operations management solutions |
| `Microsoft.PolicyInsights` | Policy compliance and remediation |
| `Microsoft.Insights` | Monitoring and data collection rules |
| `Microsoft.OperationalInsights` | Log Analytics workspaces |
| `Microsoft.Monitor` | Azure Monitor workspaces |
| `Microsoft.ManagedIdentity` | Managed identity operations |
| `Microsoft.Security` | Defender for Cloud and CSPM |
| `Microsoft.Resources` | ARM deployments |
## Validation Checklist
Before enabling EMM, verify:
- [ ] User has all 3 required roles on the subscription
- [ ] UAMI exists and has Contributor on the subscription
- [ ] Log Analytics workspace exists (or will be created)
- [ ] Azure Monitor workspace exists (or will be created)
- [ ] If cross-subscription workspaces: additional roles and RP registrations in place
README.md 1.6 KB
# {vm-name} β Bicep
Deploys a single Linux VM with VNet, subnet, NSG (SSH allow), public IP, and NIC.
## Prerequisites
- Azure CLI (`az login`)
- An existing resource group
- SSH public key at `~/.ssh/id_rsa.pub`
## Quickstart
```bash
az deployment group what-if \
--resource-group {resourceGroup} \
--template-file main.bicep \
--parameters vmName={vmName} adminUsername={adminUsername} adminPublicKey="$(cat ~/.ssh/id_rsa.pub)"
az deployment group create \
--resource-group {resourceGroup} \
--template-file main.bicep \
--parameters vmName={vmName} adminUsername={adminUsername} adminPublicKey="$(cat ~/.ssh/id_rsa.pub)"
```
## Parameters
| Name | Required | Default | Notes |
|---|---|---|---|
| `vmName` | * | β | VM resource name |
| `adminUsername` | * | β | Linux admin user |
| `adminPublicKey` | * | β | Contents of `id_rsa.pub` (secure) |
| `location` | | resourceGroup location | Azure region |
| `vmSize` | | `Standard_D2s_v5` | Verify availability with `compute_vm_list-skus` |
| `osDiskSizeGb` | | `30` | |
| `osDiskType` | | `Premium_LRS` | |
| `zone` | | `''` | `1`/`2`/`3`, or empty for regional |
| `tags` | | `{}` | |
## Outputs
- `vmId` β full ARM resource ID
- `publicIpAddress` β connect with `ssh {adminUsername}@{publicIpAddress}`
## VMSS variant
Swap `Microsoft.Compute/virtualMachines` for `Microsoft.Compute/virtualMachineScaleSets@2024-07-01`, add `sku: { name: vmSize, capacity: instanceCount }`, `properties.orchestrationMode: 'Flexible'`, and move `osProfile`/`storageProfile`/`networkProfile` inside `properties.virtualMachineProfile`.
## Cleanup
```bash
az group delete --name {resourceGroup} --yes --no-wait
```
main.bicep 3.8 KB
@description('Name of the VM')
param vmName string
@description('Azure region')
param location string = resourceGroup().location
@description('VM size, e.g. Standard_D2s_v5')
param vmSize string = 'Standard_D2s_v5'
@description('Admin username')
param adminUsername string
@description('SSH public key contents')
@secure()
param adminPublicKey string
@description('Address space for the new VNet')
param vnetAddressPrefix string = '10.0.0.0/16'
@description('Subnet prefix')
param subnetAddressPrefix string = '10.0.0.0/24'
@description('OS disk size in GB')
param osDiskSizeGb int = 30
@description('OS disk storage type')
param osDiskType string = 'Premium_LRS'
@description('Availability zone (1, 2, or 3); empty for regional')
param zone string = ''
@description('Tags applied to all resources')
param tags object = {}
@description('Source address prefix allowed for SSH inbound (CIDR or IP). Required β supply your public IP (e.g. "203.0.113.42/32") or a trusted CIDR range. "*" exposes port 22 to the entire internet; only pass it explicitly when you have accepted that risk.')
param sshSourceAddressPrefix string
var vnetName = '${vmName}-vnet'
var subnetName = 'default'
var nsgName = '${vmName}-nsg'
var publicIpName = '${vmName}-ip'
var nicName = '${vmName}-nic'
resource nsg 'Microsoft.Network/networkSecurityGroups@2024-05-01' = {
name: nsgName
location: location
tags: tags
properties: {
securityRules: [
{
name: 'AllowSSH'
properties: {
priority: 1000
access: 'Allow'
direction: 'Inbound'
protocol: 'Tcp'
sourceAddressPrefix: sshSourceAddressPrefix
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '22'
}
}
]
}
}
resource vnet 'Microsoft.Network/virtualNetworks@2024-05-01' = {
name: vnetName
location: location
tags: tags
properties: {
addressSpace: { addressPrefixes: [vnetAddressPrefix] }
subnets: [
{
name: subnetName
properties: {
addressPrefix: subnetAddressPrefix
networkSecurityGroup: { id: nsg.id }
}
}
]
}
}
resource publicIp 'Microsoft.Network/publicIPAddresses@2024-05-01' = {
name: publicIpName
location: location
tags: tags
sku: { name: 'Standard' }
properties: { publicIPAllocationMethod: 'Static' }
}
resource nic 'Microsoft.Network/networkInterfaces@2024-05-01' = {
name: nicName
location: location
tags: tags
properties: {
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
subnet: { id: '${vnet.id}/subnets/${subnetName}' }
publicIPAddress: { id: publicIp.id }
privateIPAllocationMethod: 'Dynamic'
}
}
]
}
}
resource vm 'Microsoft.Compute/virtualMachines@2024-07-01' = {
name: vmName
location: location
tags: tags
zones: empty(zone) ? null : [zone]
properties: {
hardwareProfile: { vmSize: vmSize }
storageProfile: {
imageReference: {
publisher: 'Canonical'
offer: 'ubuntu-24_04-lts'
sku: 'server'
version: 'latest'
}
osDisk: {
createOption: 'FromImage'
diskSizeGB: osDiskSizeGb
managedDisk: { storageAccountType: osDiskType }
}
}
osProfile: {
computerName: vmName
adminUsername: adminUsername
linuxConfiguration: {
disablePasswordAuthentication: true
ssh: {
publicKeys: [
{
path: '/home/${adminUsername}/.ssh/authorized_keys'
keyData: adminPublicKey
}
]
}
}
}
networkProfile: {
networkInterfaces: [
{ id: nic.id }
]
}
}
}
output vmId string = vm.id
output publicIpAddress string = publicIp.properties.ipAddress
README.md 2.5 KB
# {vm-name} β Terraform
Deploys a Linux VM (RG, VNet, subnet, NSG with SSH allow, public IP, NIC).
## Prerequisites
- `terraform >= 1.5`
- `az login`
- Exported `AZ_SUB=<subscription-id>` env var
- SSH public key at `~/.ssh/id_rsa.pub`
## Quickstart
```bash
MY_IP=$(curl -s ifconfig.me)/32 # your current public IP, locked to /32
terraform init
terraform plan -var "vm_name=dev-vm" -var "admin_public_key=$(cat ~/.ssh/id_rsa.pub)" -var "subscription_id=$AZ_SUB" -var "resource_group_name=dev-vm-rg" -var "ssh_source_address_prefix=$MY_IP"
terraform apply -var "vm_name=dev-vm" -var "admin_public_key=$(cat ~/.ssh/id_rsa.pub)" -var "subscription_id=$AZ_SUB" -var "resource_group_name=dev-vm-rg" -var "ssh_source_address_prefix=$MY_IP"
```
## Variables (see `variables.tf`)
| Variable | Type | Default | Notes |
|---|---|---|---|
| `subscription_id` * | string | β | Azure subscription |
| `resource_group_name` * | string | β | RG will be created |
| `vm_name` * | string | β | VM resource name |
| `admin_public_key` * | string (sensitive) | β | Contents of `id_rsa.pub` |
| `ssh_source_address_prefix` * | string | β | Your public IP as `<ip>/32` or a trusted CIDR. `"*"` opens port 22 to the internet β only pass it if you have accepted that risk. |
| `location` | string | `eastus` | Azure region |
| `size` | string | `Standard_D2s_v5` | Verify with `compute_vm_list-skus` |
| `admin_username` | string | `azureuser` | |
| `zone` | string | `""` | `1`/`2`/`3`, or empty for regional |
| `os_disk_type` | string | `Premium_LRS` | |
| `os_disk_size_gb` | number | `30` | |
| `tags` | map(string) | `{}` | |
`*` = required (no default).
## Outputs (see `outputs.tf`)
- `vm_id` β full ARM resource ID
- `public_ip` β connect with `ssh {admin_username}@{public_ip}`
## VMSS variant
Replace `azurerm_linux_virtual_machine` with `azurerm_linux_virtual_machine_scale_set`; add `instances`, `upgrade_mode = "Manual" | "Automatic" | "Rolling"`. Inline NIC inside the scale set via `network_interface { ip_configuration { ... } }`.
## Notes
`ssh_source_address_prefix` is required because an open SSH port is a credential-stuffing target within minutes of going public. Always pass `<your-ip>/32` (or a trusted CIDR) β even for dev. For production, also add managed identity, diagnostics, and backup.
## Cleanup
```bash
terraform destroy -var "vm_name=dev-vm" -var "admin_public_key=$(cat ~/.ssh/id_rsa.pub)" -var "subscription_id=$AZ_SUB" -var "resource_group_name=dev-vm-rg" -var "ssh_source_address_prefix=$MY_IP" -auto-approve
```
main.tf 3.2 KB
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
subscription_id = var.subscription_id
}
resource "azurerm_resource_group" "main" {
name = var.resource_group_name
location = var.location
tags = var.tags
}
resource "azurerm_network_security_group" "main" {
name = "${var.vm_name}-nsg"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
tags = var.tags
security_rule {
name = "AllowSSH"
priority = 1000
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = var.ssh_source_address_prefix
destination_address_prefix = "*"
}
}
resource "azurerm_virtual_network" "main" {
name = "${var.vm_name}-vnet"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
tags = var.tags
}
resource "azurerm_subnet" "main" {
name = "default"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.0.0/24"]
}
resource "azurerm_subnet_network_security_group_association" "main" {
subnet_id = azurerm_subnet.main.id
network_security_group_id = azurerm_network_security_group.main.id
}
resource "azurerm_public_ip" "main" {
name = "${var.vm_name}-ip"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
allocation_method = "Static"
sku = "Standard"
tags = var.tags
}
resource "azurerm_network_interface" "main" {
name = "${var.vm_name}-nic"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
tags = var.tags
ip_configuration {
name = "ipconfig1"
subnet_id = azurerm_subnet.main.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.main.id
}
}
resource "azurerm_linux_virtual_machine" "main" {
name = var.vm_name
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
size = var.size
admin_username = var.admin_username
network_interface_ids = [azurerm_network_interface.main.id]
zone = var.zone == "" ? null : var.zone
tags = var.tags
admin_ssh_key {
username = var.admin_username
public_key = var.admin_public_key
}
os_disk {
caching = "ReadWrite"
storage_account_type = var.os_disk_type
disk_size_gb = var.os_disk_size_gb
}
source_image_reference {
publisher = "Canonical"
offer = "ubuntu-24_04-lts"
sku = "server"
version = "latest"
}
}
outputs.tf 0.1 KB
output "vm_id" {
value = azurerm_linux_virtual_machine.main.id
}
output "public_ip" {
value = azurerm_public_ip.main.ip_address
}
variables.tf 1.0 KB
variable "subscription_id" {
type = string
}
variable "resource_group_name" {
type = string
}
variable "location" {
type = string
default = "eastus"
}
variable "vm_name" {
type = string
}
variable "size" {
type = string
default = "Standard_D2s_v5"
}
variable "admin_username" {
type = string
default = "azureuser"
}
variable "admin_public_key" {
type = string
sensitive = true
}
variable "zone" {
type = string
default = ""
}
variable "os_disk_type" {
type = string
default = "Premium_LRS"
}
variable "os_disk_size_gb" {
type = number
default = 30
}
variable "tags" {
type = map(string)
default = {}
}
variable "ssh_source_address_prefix" {
description = "Source address prefix allowed for SSH inbound (CIDR or IP). Required β supply your public IP (e.g. \"203.0.113.42/32\") or a trusted CIDR range. \"*\" exposes port 22 to the entire internet; only pass it explicitly when you have accepted that risk."
type = string
}
github-pr.md 3.2 KB
# Mode C β Sync to a GitHub repo
## Step 1 β Gather repo + branch + path
Ask three things (batch into one message; accept all three at once or walk through them):
1. **Repo** β accept any of:
- `owner/name` (e.g., `myorg/azure-infra`)
- Full URL (e.g., `https://github.com/myorg/azure-infra`)
- Local checkout path (e.g., `~/source/azure-infra`) β skips clone
2. **Branch** β default: `infra/vm-{vm-name}`. **Refuse to commit directly to `main` / `master` / default.** Always a branch + PR.
3. **Target path inside repo** β default: `infra/{vm-name}/`. If the repo has a recognizable infra root (`/terraform`, `/bicep`, `/infrastructure`), suggest that.
## Step 2 β Pre-flight
```bash
# 1. gh installed and authenticated
gh auth status # must succeed; if not, fall back to Mode C-fallback below
# 2. user has push rights
gh repo view {owner/repo} --json viewerPermission --jq '.viewerPermission'
# Must be ADMIN, MAINTAIN, or WRITE; otherwise pivot to fork-and-PR
# 3. branch doesn't already exist
gh api repos/{owner/repo}/branches/{branch} # 404 = good (new branch)
```
If `gh` is missing or unauthenticated, **switch to Mode C-fallback** β don't block.
## Step 3 β Clone, write, commit, push, PR
Echo each command so the user can replay:
```bash
# 1. Working tree
TMP=$(mktemp -d) && cd "$TMP"
gh repo clone {owner/repo} .
# 2. Branch off the default branch
DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')
git checkout -b {branch} "origin/$DEFAULT_BRANCH"
# 3. Write the artifact files (same layout as Mode B)
mkdir -p {targetPath}
# ... Write tool calls for each file ...
# 4. Stage, commit, push
git add {targetPath}
git commit -m "Add {vm-name} VM infrastructure (Bicep)" \
-m "Generated by azure-compute vm-creator workflow."
git push -u origin {branch}
# 5. Open the PR
gh pr create \
--title "Add {vm-name} VM infrastructure" \
--body "$(cat <<'EOF'
## Summary
Adds Bicep templates to provision \`{vm-name}\` in \`{location}\`.
## Plan Card
{paste-plan-card-markdown}
## Deploy
\`\`\`bash
cd {targetPath}
az deployment group create --resource-group {resourceGroup} \\
--template-file main.bicep --parameters vmName={vm-name} ...
\`\`\`
EOF
)" \
--base "$DEFAULT_BRANCH" --head {branch}
```
Echo the PR URL back:
> β
Opened PR: https://github.com/myorg/azure-infra/pull/42
## Step 4 β Optional follow-ups
After the PR is open, **offer** (don't force):
- *"Want me to add a GitHub Action to validate this with `az deployment group what-if` on every PR?"* β generates `.github/workflows/bicep-whatif.yml`
- *"Want OIDC auth so CI can apply without a service principal secret?"* β points at the `azure-prepare` skill
## Mode C-fallback (no `gh` / unauthenticated / no push rights)
Write the files to `~/Desktop/{vm-name}-infra/` (Mode B path) and print:
> β I can't push for you (`gh auth status` failed), so I wrote the files locally.
>
> To open the PR yourself:
> ```bash
> cd ~/Desktop/dev-vm-infra
> git init && git add . && git commit -m "Add dev-vm infrastructure"
> git remote add origin git@github.com:myorg/azure-infra.git
> git checkout -b infra/vm-dev-vm
> git push -u origin infra/vm-dev-vm
> gh pr create --fill # after `gh auth login`
> ```
index.md 2.3 KB
# Delivery Options
After the Plan Card is approved (Step 5) and the output format is chosen (Step 6), the workflow has a generated artifact. **Before printing it into the chat**, ask one more question β where should it go?
| Mode | When the user wants this | File |
|---|---|---|
| **A. Print in chat** | Quick one-off, copy-paste, just learning | [print.md](print.md) |
| **B. Save to local folder** | Wants files on disk, will commit/run later | [save-local.md](save-local.md) |
| **C. Sync to a GitHub repo** | Has an infra repo, wants the change as a PR | [github-pr.md](github-pr.md) |
> **Skip the question for Adapter 4 (Apply via MCP).** Delivery is moot β the resources get created in Azure directly.
## Asking
> *"Where should I drop this {format} artifact?"*
> *1. Print in the chat (default for quick / one-off)*
> *2. Save to a local folder β I'll suggest a path based on your current workspace*
> *3. Sync to a GitHub repo as a new branch + PR*
If the user already implied a target ("save it to my infra repo" / "open a PR" / "just print it"), skip the question and use that mode.
## Re-emitting / changing delivery mid-flow
After delivery, the Plan Card is cached. The user can say:
| User says | Action |
|---|---|
| "also save it locally" | Re-run Mode B with the same artifact |
| "open a PR instead" | Re-run Mode C |
| "give me terraform now too" | Re-render that adapter, then re-ask delivery |
| "delete that file you wrote" | `rm` the path that was written |
Never re-ask Plan Card questions when only the delivery target is changing.
## Safety checks (B and C)
| Check | Why |
|---|---|
| Don't overwrite without showing the diff and getting explicit yes | User may have manual edits |
| Scrub `adminPassword` from generated files β replace with `${VM_ADMIN_PASSWORD}` env var | Repos leak passwords |
| Never push directly to `main` / `master` / default branch β feature branch + PR only | Auditability and review |
| Never `git push --force` (use `--force-with-lease` if a rewrite is truly needed) | Catastrophic on shared branches |
| Emit a parameter placeholder for SSH keys, not the literal key contents | Keys committed to git stay there forever |
| If the repo has `CODEOWNERS` for the path, mention who'll be auto-tagged | User shouldn't be surprised by review routing |
print.md 0.5 KB
# Mode A β Print in chat
Default. Render fenced code blocks for each file, in this order:
1. **Bicep:** `main.bicep` + the `az deployment group create` command at the bottom
2. **Terraform:** `main.tf` + `variables.tf` (separate fenced blocks) + the `terraform init && terraform apply` command
3. **bash:** the single script
Append a one-liner reminder: *"Want me to save this to disk or open a PR? Just ask."* β so the user can shift to Mode B/C without restarting.
save-local.md 2.0 KB
# Mode B β Save to a local folder
## Step 1 β Suggest a path
Detect the user's current working context (in priority order; pick the **first** that succeeds):
| Signal | Suggested path |
|---|---|
| Host session has an `--add-dir` workspace containing a `.git` directory | `<workspace>/infra/{vm-name}/` |
| User mentioned a repo path earlier in the conversation | `<that-path>/infra/{vm-name}/` |
| `pwd` is inside a git repo | `<repo-root>/infra/{vm-name}/` |
| None of the above | `~/Desktop/{vm-name}-infra/` |
Present the suggestion and let the user override:
> *"I'll save to `~/Desktop/dev-vm-infra/` (no repo detected). Use that, or pick another path?"*
If the path already exists with files, **always show the diff and ask before overwriting**. Never silently clobber.
## Step 2 β Choose the filename(s)
| Format | Files written |
|---|---|
| bash | `create-vm.sh` (chmod +x) |
| Bicep | `main.bicep`, `README.md` |
| Terraform | `main.tf`, `variables.tf`, `outputs.tf`, `terraform.tfvars.example`, `README.md` |
The `README.md` contains:
- The full **Plan Card** markdown (so the next reader knows what this deploys)
- **Deploy** section with exact commands (`az deployment group create ...` / `terraform init && apply ...` / `bash create-vm.sh`)
- **Verify** section: `az vm show` / `az vm list-ip-addresses`
- **Cleanup** section: `az group delete --name <rg> --yes --no-wait`
## Step 3 β Write and confirm
After writing, echo back absolute paths and the single command the user runs next:
> β
Wrote 2 files:
> - `~/source/my-infra/infra/dev-vm/main.bicep`
> - `~/source/my-infra/infra/dev-vm/README.md`
>
> Next step:
> ```bash
> cd ~/source/my-infra/infra/dev-vm
> az deployment group create --resource-group dev-vm-rg --template-file main.bicep \
> --parameters vmName=dev-vm adminUsername=azureuser \
> adminPublicKey="$(cat ~/.ssh/id_rsa.pub)"
> ```
## Tool implementation
Use the `Write` tool (or the host's equivalent) for each file. For shell scripts: write, then `chmod +x` via Bash.
beginner.md 1.2 KB
# Beginner / fast-path
Goal: get to a working Plan Card in **β€ 2 questions**, then show defaults and let the user edit.
| # | Question | Default if skipped |
|---|---|---|
| 1 | "What region? I can recommend if you're not sure." | `eastus` |
| 2 | "Linux or Windows? Default is Ubuntu 24.04." | `Ubuntu2404` (Linux) |
## Silent defaults (show in Plan Card, don't ask)
- **Size:** `Standard_D2s_v5` (2 vCPU / 8 GB)
- **Auth:** SSH key from `~/.ssh/id_rsa.pub` (Linux) β read the file; ask only if missing
- **VNet:** create new `<vm-name>-vnet` with `10.0.0.0/16`
- **Subnet:** `default` with `10.0.0.0/24`
- **NSG:** create new, allow SSH 22 (Linux) or RDP 3389 (Windows) from **the user's current public IP** (detect via `curl -s ifconfig.me` or equivalent) β only fall back to `*` if detection fails, and always flag the chosen source in the Plan Card with a β so the user can edit before apply
- **Public IP:** Standard SKU, dynamic
- **OS disk:** 30 GB Premium SSD
- **Zone:** none (regional)
If the user is in a region you haven't validated, call `compute_vm_list-skus` to confirm `Standard_D2s_v5` is available there before locking it in. If not, fall back to whatever the recommender suggests.
cost-deep.md 1.1 KB
# Cost-deep branch
| Topic | Question | Default |
|---|---|---|
| Spot vs regular | "Spot eligible (interruptible)?" | `regular` unless dev / batch |
| Spot max price | "Max spot price ($/hr) or pay up to on-demand?" | `-1` (= on-demand cap) |
| Reservations | "1-yr / 3-yr reservation?" | Skip β recommend post-deploy when usage is known |
| Hybrid Benefit | "Bring Windows Server / RHEL / SLES license?" | Ask only if OS is Windows or RHEL |
| Autoscale floor/ceiling (VMSS) | "Min / max instances?" | `min=1, max=3` for web tier; `min=0, max=10` for batch |
| Schedule shutdown | "Auto-shutdown nightly?" | Offer for dev / sandbox workloads |
| Disk tier | "OS disk tier: Premium SSD / Standard SSD / Standard HDD?" | Premium SSD |
## Notes
- Spot interruption rates vary by region and SKU; mention the user can check before committing via the Azure portal "Spot eviction rate" view.
- Reservations and savings plans need 30+ days of usage telemetry to recommend confidently β don't push them on a brand-new workload.
- Auto-shutdown via DevTest Labs is the cheapest scheduled-stop option for single VMs; for VMSS, scale-to-zero is better.
index.md 2.9 KB
# Depth Probe β Meet the User Where They Are
The VM Creator adapts its questioning to the user's expertise and intent. A beginner asking for a "dev VM" should not get peppered with networking and egress questions. An advanced networking engineer specifying "VMSS behind App Gateway with private endpoints" should not be asked whether they want a public IP.
## Philosophy
1. **Never ask a question whose answer can be inferred or safely defaulted.**
2. **Batch silent inferences into a Plan Card.** Defaulted decisions should be visible and editable.
3. **Defaults ladder.** When you must ask, prefer `[recommended default] / [show alternatives] / [I have specifics]`.
4. **Branching is signal-driven, not flag-driven.** Reclassify any time the user volunteers a deep signal.
## Classification β read the initial request
Score each signal that appears in the user's first 1-2 messages. The highest-scoring branch wins; a user can be in multiple branches.
| Signal phrase / keyword | Branch |
|---|---|
| "VNet", "subnet", "NSG", "egress", "private endpoint", "App Gateway", "accelerated networking", "service tag", "UDR", "IPv6", "DNS", "Bastion" | [networking-deep](networking-deep.md) |
| "vCPUs", "GPU", "memory", "family", "D-series", "N-series", "ephemeral OS disk", "proximity placement", "AMD", "Intel", "generation", "SR-IOV", "trusted launch" | [spec-deep](spec-deep.md) |
| "spot", "reserved", "savings plan", "hybrid benefit", "autoscale floor/ceiling", "$", "budget", "cheapest", "cost-optimize" | [cost-deep](cost-deep.md) |
| "managed identity", "Entra", "RBAC", "Key Vault", "JIT", "encryption at host", "CMK", "confidential", "compliance", "FedRAMP", "HIPAA" | [security-deep](security-deep.md) |
| "dev", "sandbox", "quick", "test out", "play with", "just need", "simple", or nothing specific | [beginner](beginner.md) |
> **Tiebreak:** prefer the branch that affects the most expensive defaults: Networking > Security > Spec > Cost > Beginner. Networking mistakes are the hardest to undo post-deployment.
## Cross-branch follow-ups (ask once, after primary branch)
| Question | When to ask |
|---|---|
| "Tags? (env, owner, cost-center)" | Always β but accept "none" without follow-up |
| "Resource group: existing or new?" | Always β propose `<vm-name>-rg` if new |
| "Number of instances?" | Only for VMSS |
| "Orchestration mode (Flexible/Uniform)?" | Only for VMSS β default Flexible |
## Reclassification mid-flow
If the user volunteers a deep signal at any step ("oh wait, I also need a NAT Gateway"), jump into that branch's questions for that topic. Never restart the whole flow β append the new questions and update the Plan Card.
## Anti-patterns
- β Asking "what OS?" when the user said "Ubuntu sandbox"
- β Asking about spot pricing for a Windows production VM
- β Asking 8 networking questions before showing a Plan Card
- β Defaulting to public IP open to `*` without flagging it in the Plan Card
- β Burying the cost estimate at the bottom β put it on the Plan Card top row
networking-deep.md 1.6 KB
# Networking-deep branch
Ask only what cannot be inferred. Volunteer the advanced switches.
| Topic | Question | Default offered |
|---|---|---|
| VNet | "Existing VNet, or new?" | If existing: ask name + RG; offer to list via `network_vnet_list` MCP or `az network vnet list` |
| Subnet sizing | "Subnet CIDR?" | `/24` if new |
| NSG | "Inbound rules: default (SSH/RDP from your IP) or paste a rule set?" | Restrict source to user's current public IP β fetch via `curl -s ifconfig.me` if not provided |
| Public IP | "Public IP, or private only?" | Public unless user said "private", "internal", "no internet" |
| Accelerated networking | "Enable accelerated networking?" | `true` if size supports it (most D/E/F series β₯ 2 vCPU) |
| Private endpoints | "Any private endpoints to attach?" | Not by default; ask only if user mentioned data / Key Vault / storage targets |
| Outbound | "Outbound: default Azure SNAT, NAT Gateway, or Firewall route?" | Default SNAT if user didn't mention egress; if mentioned, default NAT Gateway |
| DNS | "Custom DNS servers?" | Azure-provided |
| IP version | "IPv4 only or dual-stack?" | IPv4 |
| Service endpoints | "Service endpoints on subnet?" | None unless user mentioned a target |
## Notes
- Don't auto-create a NAT Gateway just because the user said "secure" β confirm intent first; NAT Gateway is ~$30/mo before traffic.
- If the user wants "private only", offer Azure Bastion as the management path; don't silently leave them with no way in.
- `accelerated networking` defaults to **on** for supporting SKUs because the cost is zero and the throughput gain is large.
security-deep.md 1.2 KB
# Security-deep branch
| Topic | Question | Default |
|---|---|---|
| Managed identity | "System-assigned managed identity?" | `true` (off by default in raw `az vm create`, but we recommend on) |
| Encryption at host | "Encryption at host?" | `true` (requires subscription opt-in β flag if not enabled) |
| Disk encryption set | "Customer-managed key (CMK) on OS disk?" | Skip unless compliance mentioned |
| Confidential VM | "Confidential compute (AMD SEV-SNP)?" | Only if user mentioned `confidential` / `attestation` |
| JIT access | "Enable Just-In-Time RDP/SSH (Defender for Cloud)?" | Offer if subscription has Defender plan |
| Boot diagnostics | "Managed boot diagnostics?" | `true` (Azure-managed storage) |
| Vulnerability scanning | "Enable Defender for Servers Plan 2?" | Mention; do not auto-enable (incurs cost) |
## Notes
- Encryption-at-host needs the subscription feature flag `EncryptionAtHost` registered β check via `az feature show` and surface a remediation step if not.
- CMK setup is multi-resource (Key Vault + Disk Encryption Set + RBAC); for first-time users, suggest scaffolding via the `azure-prepare` skill instead.
- JIT access is per-VM and per-port; default to 3-hour windows on 22/3389, not the wider "all common ports" preset.
spec-deep.md 1.3 KB
# Spec-deep branch
Goal: nail the SKU. Lean on `compute_vm_list-skus` (with filters) instead of asking the user to memorize SKU names.
| Topic | Question | How to answer |
|---|---|---|
| vCPUs / RAM | "Target vCPUs and memory?" | Call `compute_vm_list-skus` with `minVCpus` + `minMemoryGb`; show top 3 |
| GPU | "GPU workload?" | If yes: `familyPrefix=Standard_N`; ask CUDA vs render |
| Family | "Family preference (general D, compute F, memory E, burstable B, GPU N)?" | Skip if vCPU/RAM already nailed it down |
| Generation | "VM generation (v5/v6, AMD/Intel)?" | Default to latest gen available in region |
| Ephemeral OS disk | "Ephemeral OS disk (faster, but no resize/restore)?" | `false` unless workload is stateless tier |
| Trusted Launch / Gen2 | "Gen2 / Trusted Launch?" | `true` (Azure default for new VMs since 2023) |
| Proximity placement | "Need low-latency between VMs?" | Skip unless multi-VM cluster |
| Zone | "Pin to an Availability Zone?" | Skip for single VM unless HA; default `1` for VMSS spreading |
## Notes
- For GPU asks, always include the per-hour price in the Plan Card β N-series VMs can hit $3-30/hr.
- If the user says "compute-heavy", default to F-series (compute-optimized) before D-series.
- Burstable B-series is only correct for spiky/idle workloads β flag it explicitly so the user knows it'll throttle under sustained load.
mcp-tools.md 2.3 KB
# MCP tools used by vm-creator
The Azure MCP server exposes the `compute` area as a single namespace proxy: `mcp__azure__compute({intent, command, parameters})`. The commands below are what the workflow dispatches against it.
## Read-only validation (Step 4)
| Command | Purpose | Key parameters |
|---|---|---|
| `compute_vm_list-skus` | Confirm SKU availability + filter by vCPU/memory/family | `subscription`, `location`, `minVCpus`, `minMemoryGb`, `familyPrefix`, `top` |
| `compute_vm_list-images` | Resolve image alias / URN | `subscription`, `location`, `publisher`, `offer`, `sku` |
| `compute_vm_check-quota` | Verify vCPU headroom for the family | `subscription`, `location`, `family` |
| `compute_vm_recommend-region` | Find regions where the workload fits | workload hints; returns ranked regions |
## Apply (Step 6, Adapter 4)
| Command | Purpose |
|---|---|
| `compute_vm_create` | Create a single VM from Plan Card fields |
| `compute_vmss_create` | Create a VMSS (adds `instance-count`, `upgrade-policy`) |
| `compute_vm_get` | Inspect after create |
| `compute_vm_update` | Tag changes, size resize, identity attach |
| `compute_vm_delete` | Cleanup |
See [output-adapters/mcp-apply.md](output-adapters/mcp-apply.md) for the full parameter mapping and failure-handling table.
## CLI fallbacks (when Azure MCP is not connected)
| MCP command | CLI equivalent |
|---|---|
| `compute_vm_list-skus` | `az vm list-skus --location <region> --output table` |
| `compute_vm_list-images` | `az vm image list --location <region> --offer <offer> --all` |
| `compute_vm_check-quota` | `az vm list-usage --location <region> --output table` |
| `compute_vm_recommend-region` | (no CLI equivalent β fall back to docs) |
| `compute_vm_create` | `az vm create ...` (see [az-cli.md](output-adapters/az-cli.md)) |
## Why the proxy form matters
The CLI / tool host shows `mcp__azure__compute` as a single tool. Sub-operations like `vm check-quota`, `vm list-skus`, etc. are not separate tools β they are passed through the `command` parameter. Every command is invoked as:
```
mcp__azure__compute({
command: "vm check-quota",
parameters: { location: "eastus", family: "standardDSv5Family" }
})
```
When tracing tool calls or writing must-call rubrics, look for the `command=` argument, not a distinct tool name.
plan-card.md 3.9 KB
# Plan Card
The Plan Card is the single source of truth for the create-flow. It renders **every decision** β explicit user answers and silent defaults β as a markdown table the user can read top-to-bottom and either approve or edit.
## Rendering rules
1. **Cost + quota on the top half.** The user must see both without scrolling.
2. **Source column is mandatory.** Each row says where the value came from: `user`, `default`, `inferred`, or the MCP tool that produced it.
3. **Flag risky defaults with β .** Open NSG to `*`, public IP exposed, no managed identity, password auth on Windows β all get a marker so the user can edit.
4. **No invisible state.** If you defaulted it, it goes in the card.
5. **Re-emit on every change.** When the user edits a row, render the full card again β diffs in chat are easy to miss.
## Schema
| Column | Required? | Notes |
|---|---|---|
| Setting | yes | Human-readable label (`Region`, `Size`, `OS disk`) |
| Value | yes | The concrete value, in backticks if it's a literal |
| Source | yes | `user` / `default` / `inferred` / `<tool name>` |
## Example β Linux dev VM in eastus
```markdown
| Setting | Value | Source |
|---|---|---|
| Hosting model | Single VM | user |
| Name | `dev-vm-01` | default (`<purpose>-vm-<nn>`) |
| Region | `eastus` | user |
| Resource group | `dev-vm-01-rg` (new) | default |
| Image | `Ubuntu2404` | user |
| Size | `Standard_D2s_v5` (2 vCPU / 8 GB) | default |
| Auth | SSH key from `~/.ssh/id_rsa.pub` | inferred |
| VNet | new `dev-vm-01-vnet` (`10.0.0.0/16`) | default |
| Subnet | `default` (`10.0.0.0/24`) | default |
| NSG | SSH 22 from your public IP (`203.0.113.42`) | default β β change to `*` only if needed |
| Public IP | Standard, dynamic | default |
| OS disk | 30 GB Premium SSD | default |
| Boot diagnostics | Managed | default |
| Estimated cost | ~$0.096/hr (~$70/mo) | from `compute_vm_list-skus` |
| Quota | β
4/100 vCPUs used in `standardDSv5Family` | from `compute_vm_check-quota` |
```
## After rendering β single batched action picker
Render the Plan Card markdown **inline in the chat first** (so the user can read it), then ask **one** AskUserQuestion that combines approval + output format + delivery:
> *"Looks good? Pick how you want it delivered:"*
>
> 1. **Save Bicep to `./infra/{vm-name}/`** *(Recommended for repos)*
> 2. **Print az CLI in chat** *(Quick copy-paste)*
> 3. **Save Terraform to `./infra/{vm-name}/`**
> 4. **Open GitHub PR with Bicep**
> 5. **Apply live via Azure MCP** *(actually creates resources)*
> 6. **Edit a row first** *(then re-render and re-ask)*
This collapses what used to be 3 sequential popups (approve β output format β delivery) into **1**.
Implementation: a single `AskUserQuestion` tool call with `header: "Deliver"`, `multiSelect: false`, and 6 options (the most likely combinations above) β the user can also pick "Other" to type a custom answer like "give me both bicep and terraform".
**If the user picks "Edit a row first":** then ask which row, update, re-render the full Plan Card, and re-ask the same batched action picker. Do not splinter into multiple popups.
**If the user already implied the answer in their original prompt** ("save bicep to ./infra" / "open a PR" / "just print az CLI" / "apply it"): **skip this prompt entirely** and proceed straight to delivery.
**Explicit-override fast path β skip the Plan Card table too.** If the user combines an explicit deliverable ("give me the Bicep", "just print az CLI") with an explicit refusal of dialog ("no questions", "skip planning", "no plan", "just do it"): **do not render the Plan Card markdown table.** Instead emit a single-line preview of the high-signal decisions β e.g. *"β Deploying `Standard_D2s_v5` in `eastus`, NSG = your public IP only on 22, est. ~$70/mo"* β and follow it immediately with the requested artifact. End with a one-liner noting the full Plan Card is available on request if they want to edit rows. Step 4 validation gates still run.
validation-gates.md 2.2 KB
# Validation Gates
Step 4 of the create-flow runs four read-only checks before the Plan Card is shown. Required path: Azure MCP. Fallback: CLI patterns in [mcp-tools.md](mcp-tools.md). Do **not** substitute generic guidance tools (`get_azure_bestpractices`, `pricing`) β they don't validate quota, SKU availability, or region support.
## Checks
| Check | MCP tool | What to verify |
|---|---|---|
| SKU exists in region | `compute_vm_list-skus` (`location`, `minVCpus`, `minMemoryGb`, optional `familyPrefix`) | At least one matching SKU, no `restrictions` in target zone |
| Image is current | `compute_vm_list-images` (alias or `publisher`/`offer`/`sku`) | Alias resolves to a published URN |
| vCPU quota | `compute_vm_check-quota` (`location`, `family`) | `currentValue + requestedVCPUs β€ limit` |
| Region availability | `compute_vm_recommend-region` (workload hints) | Region exists and supports the family |
## Outcomes
| Result | Action |
|---|---|
| β
Sufficient | Proceed to Step 5 (Plan Card) |
| β οΈ Near limit (>80%) | Proceed but flag in Plan Card; suggest quota increase |
| β Insufficient / SKU missing | Propose alternate SKU or region; do not generate output |
## Common failures
| Symptom | Cause | Fix |
|---|---|---|
| `compute_vm_list-skus` returns empty | Filter too narrow; SKU not in region | Drop `familyPrefix`; lower `minVCpus`; try another region |
| Quota at limit | Subscription cap | Smaller SKU / different family / different region / quota-increase request |
| Image URN unresolved | Wrong alias; deprecated image | Switch to `publisher`/`offer`/`sku`/`version` form; check Marketplace |
| Region rejects family | Family not GA in region | Use `compute_vm_recommend-region` to find a region that supports the family |
## When Azure MCP is not connected
Warn the user that pre-flight checks are reduced. Use the CLI equivalents in [mcp-tools.md](mcp-tools.md):
- `az vm list-skus --location <region> --output table`
- `az vm image list --location <region> --offer ubuntu-24_04-lts --all`
- `az vm list-usage --location <region> --output table`
These don't gate the artifact generation β they're informational. Surface their output verbatim so the user can self-check.
az-cli.md 1.9 KB
# az CLI adapter
Fast, portable, scriptable. Works anywhere `az` is installed and logged in.
## VM template
```bash
#!/usr/bin/env bash
set -euo pipefail
# {plan-card-summary}
az group create --name "{resourceGroup}" --location "{location}"
az vm create \
--resource-group "{resourceGroup}" \
--name "{vmName}" \
--location "{location}" \
--image "{image}" \
--size "{vmSize}" \
--admin-username "{adminUsername}" \
--ssh-key-values @{sshKeyPath} \
--vnet-name "{vnetName}" \
--subnet "{subnetName}" \
--nsg "{nsgName}" \
--public-ip-address "{publicIpName}" \
--zone {zone} \
--os-disk-size-gb {osDiskSizeGb} \
--storage-sku {osDiskType} \
--tags {tagsKv}
```
## VMSS template
Replace `az vm create` with `az vmss create`, swap `--size` for `--vm-sku`, add `--instance-count {n}`, `--orchestration-mode Flexible`, `--upgrade-policy-mode Manual|Automatic|Rolling`.
## Filled example β dev Linux VM in eastus
```bash
#!/usr/bin/env bash
set -euo pipefail
# dev-vm | eastus | Ubuntu2404 | Standard_D2s_v5 | new VNet | est. $70/mo
az group create --name dev-vm-rg --location eastus
az vm create \
--resource-group dev-vm-rg \
--name dev-vm \
--location eastus \
--image Ubuntu2404 \
--size Standard_D2s_v5 \
--admin-username azureuser \
--ssh-key-values @~/.ssh/id_rsa.pub \
--vnet-name dev-vm-vnet \
--subnet default \
--nsg dev-vm-nsg \
--public-ip-address dev-vm-ip \
--os-disk-size-gb 30 \
--storage-sku Premium_LRS \
--tags env=dev owner=team-name
```
## Notes
- Windows VMs: swap `--ssh-key-values @...` for `--admin-password '{password}'`.
- Linux: prefer SSH keys (`~/.ssh/id_rsa.pub` or `~/.ssh/id_ed25519.pub`). Never paste private keys.
- `--zone` is optional; omit the flag entirely (don't pass empty) for regional VMs.
- `--tags` uses space-separated `k=v` pairs.
- Pre-check quota: `compute_vm_check-quota` (or `az vm list-usage --location {location} -o table`).
bicep.md 1.7 KB
# Bicep adapter
Production IaC, repeatable deployments, supports `az deployment group what-if` preview before commit.
## Template
Emit [`examples/bicep/main.bicep`](../../examples/bicep/main.bicep) alongside the README. Single-file template β no modules to wire. Parameters (`vmName`, `adminUsername`, `adminPublicKey` required; others have defaults).
Always emit `examples/bicep/README.md` next to the template so the artifact is self-contained (Plan Card, prereqs, quickstart, parameter table, cleanup).
## Deploy
```bash
az deployment group what-if \
--resource-group {resourceGroup} \
--template-file main.bicep \
--parameters vmName={vmName} adminUsername={adminUsername} \
adminPublicKey="$(cat ~/.ssh/id_rsa.pub)"
az deployment group create \
--resource-group {resourceGroup} \
--template-file main.bicep \
--parameters vmName={vmName} adminUsername={adminUsername} \
adminPublicKey="$(cat ~/.ssh/id_rsa.pub)"
```
Always run `what-if` first β it's free and surfaces any quota / role / naming conflict before the change lands.
## VMSS
Swap `Microsoft.Compute/virtualMachines@2024-07-01` for `Microsoft.Compute/virtualMachineScaleSets@2024-07-01`. Add `sku: { name: vmSize, capacity: instanceCount }`, `properties.orchestrationMode: 'Flexible'`, and move `osProfile` / `storageProfile` / `networkProfile` inside `properties.virtualMachineProfile`.
## Notes
- Secure params (`adminPassword`, `adminPublicKey`) are `@secure()`; don't echo them in logs.
- For `zone`, pass `'1'`/`'2'`/`'3'` to pin a zone, or `''` for regional. The template handles both via `empty(zone) ? null : [zone]`.
- For private VMs, set `publicIPAllocationMethod` to nothing and drop the `publicIPAddress` block from the NIC.
index.md 3.0 KB
# Output Adapters
The VM Creator's final step emits the user's choice of one (or more) output formats from the same approved Plan Card. The user can re-emit a different format at any time without restarting the conversation.
## Choosing the format
| Format | When to offer | File |
|---|---|---|
| **az CLI bash** | Quick one-off, learning, ad-hoc creation, CI scripts | [az-cli.md](az-cli.md) |
| **Bicep** | Production IaC, repeatable, what-if preview, org standardized on Bicep | [bicep.md](bicep.md) |
| **Terraform** | Multi-cloud, existing TF state, org standardized on Terraform | [terraform.md](terraform.md) |
| **Apply via Azure MCP** | "Just do it" β user is in an MCP-connected host and trusts the Plan Card | [mcp-apply.md](mcp-apply.md) |
Always show the user all four as a numbered choice at Step 6. Default-suggest based on signals:
- "I want a script" β az CLI
- "I want infra-as-code" β Bicep (default) / Terraform (if user mentioned TF)
- "Just create it" / "deploy it now" β Apply via Azure MCP
## Plan Card β parameter mapping
Every adapter draws from these Plan Card fields. Capture them once; transform on emit.
| Plan Card field | az CLI | Bicep | Terraform | MCP parameter |
|---|---|---|---|---|
| name | `--name` | `vmName` | `vm_name` | `vm-name` |
| resourceGroup | `--resource-group` | (scope) | `resource_group_name` | `resource-group` |
| subscription | `--subscription` | (scope) | `subscription_id` | `subscription` |
| location | `--location` | `location` | `location` | `location` |
| size | `--size` | `vmSize` | `size` | `vm-size` |
| image | `--image` | `imageReference` | `source_image_reference` | `image` |
| adminUsername | `--admin-username` | `adminUsername` | `admin_username` | `admin-username` |
| sshKey | `--ssh-key-values` | `adminPublicKey` | `admin_ssh_key.public_key` | `ssh-public-key` |
| adminPassword | `--admin-password` | `adminPassword` (secure) | `admin_password` (sensitive) | `admin-password` |
| vnetName | `--vnet-name` | `vnetName` | `azurerm_virtual_network.name` | `virtual-network` |
| subnetName | `--subnet` | `subnetName` | `azurerm_subnet.name` | `subnet` |
| publicIp | `--public-ip-address` | `publicIpName` | `azurerm_public_ip` | `public-ip-address` |
| nsgName | `--nsg` | `nsgName` | `azurerm_network_security_group.name` | `network-security-group` |
| zone | `--zone` | `zones: [N]` | `zone` | `zone` |
| osDiskType | `--storage-sku` | `osDisk.managedDisk.storageAccountType` | `os_disk.storage_account_type` | `os-disk-type` |
| osDiskSizeGb | `--os-disk-size-gb` | `osDisk.diskSizeGB` | `os_disk.disk_size_gb` | `os-disk-size-gb` |
| tags | `--tags` | `tags` | `tags` | (none β emit separately) |
For VMSS, also map `instanceCount` and `upgradePolicy` (see each adapter file).
## Re-emitting after a format switch
After the user picks one format, save the Plan Card. If they later say "actually give me the bicep too" or "show me terraform", regenerate from the same Plan Card β do not re-ask any questions. The Plan Card is the canonical state.
mcp-apply.md 2.6 KB
# Apply via Azure MCP
User is in an MCP-connected host (Claude Code, VS Code Copilot, Cursor) with the Azure MCP server enabled and says "just do it" / "create it now" / "deploy".
## Prerequisite
Azure MCP server installed, and the user is signed in to Azure (e.g. `az login`). The exact auth mechanism is an implementation detail of the MCP server and may vary.
## Pre-flight checks (read-only)
```
compute_vm_check-quota(subscription, location, family={derived from vmSize})
compute_vm_list-skus(subscription, location, familyPrefix={derived from vmSize})
```
Confirm:
- Quota has headroom for the requested vCPU count
- The chosen SKU is available in that region (and zone, if specified)
## Apply β VM
Call `compute_vm_create` with the Plan Card values:
| MCP parameter | Plan Card field |
|---|---|
| `subscription` | subscription |
| `resource-group` | resourceGroup |
| `vm-name` | name |
| `location` | location |
| `image` | image |
| `vm-size` | size |
| `admin-username` | adminUsername |
| `ssh-public-key` (Linux) / `admin-password` (Windows) | sshKey / adminPassword |
| `virtual-network` | vnetName (omit to auto-create) |
| `subnet` | subnetName (omit to auto-create) |
| `network-security-group` | nsgName (omit to auto-create) |
| `public-ip-address` | publicIpName |
| `no-public-ip` | true if Plan Card says private only |
| `source-address-prefix` | restrict NSG inbound source (e.g., user's IP) |
| `zone` | zone |
| `os-disk-size-gb` | osDiskSizeGb |
| `os-disk-type` | osDiskType |
| `os-type` | linux / windows (usually auto-detected from image) |
## Apply β VMSS
Call `compute_vmss_create` with the same fields, plus:
| MCP parameter | Plan Card field |
|---|---|
| `vmss-name` | name |
| `instance-count` | instanceCount |
| `upgrade-policy` | upgradePolicy (Manual / Automatic / Rolling) |
## After apply
- Tool returns `VmCreateResult` / `VmssCreateResult` with `Id`, `Name`, `Location`, `VmSize`, `ProvisioningState`, `PublicIpAddress`, `PrivateIpAddress`, `Zones`, `Tags`.
- Echo back to user: hostname / IP / SSH command (`ssh {adminUsername}@{publicIp}`) or RDP command (`mstsc /v:{publicIp}`).
- Offer next steps: list/inspect (`compute_vm_get`), update (`compute_vm_update`), delete (`compute_vm_delete`).
## Failure handling
| Error | Action |
|---|---|
| `Quota exceeded` | re-run `compute_vm_check-quota`; suggest smaller SKU or different family |
| `A VM with the specified name already exists` | ask for a new name |
| `Resource not found` on RG | create the RG first (`group_create` MCP or `az group create`) |
| `Authorization failed` | user needs Contributor or VM Contributor on the RG |
terraform.md 1.5 KB
# Terraform adapter
Multi-cloud, existing TF state, organization standardized on Terraform.
## Templates
Emit three code files to [`examples/terraform/`](../../examples/terraform/README.md):
- `main.tf` β provider, RG, VNet, subnet, NSG (SSH allow), public IP, NIC, Linux VM
- `variables.tf` β typed input variables with defaults
- `outputs.tf` β `vm_id`, `public_ip`
Always emit `examples/terraform/README.md` (Plan Card, prereqs, quickstart, variables table, outputs, cleanup). When the user requests a PR (Mode C), the same README becomes the PR body.
## Deploy
```bash
terraform init
terraform plan -var "vm_name={vmName}" -var "admin_public_key=$(cat ~/.ssh/id_rsa.pub)" \
-var "subscription_id=$AZ_SUB" -var "resource_group_name={resourceGroup}"
terraform apply -var "vm_name={vmName}" -var "admin_public_key=$(cat ~/.ssh/id_rsa.pub)" \
-var "subscription_id=$AZ_SUB" -var "resource_group_name={resourceGroup}"
```
## VMSS
Replace `azurerm_linux_virtual_machine` with `azurerm_linux_virtual_machine_scale_set` (or Windows variants). Add `instances`, `upgrade_mode = "Manual" | "Automatic" | "Rolling"`. NIC moves inline inside the scale set resource via `network_interface { ip_configuration { ... } }`.
## Notes
- Provider version pinned to `~> 4.0` β bump deliberately, not implicitly.
- `admin_public_key` is `sensitive = true`; don't print it.
- `zone` is `""` by default (regional); to pin, pass `"1"`, `"2"`, or `"3"`.
- Pre-check quota with `compute_vm_check-quota` before `terraform apply`.
vm-creator.md 7.4 KB
# Azure VM/VMSS Creator
Guided create-flow for Azure Virtual Machines (VMs) and VM Scale Sets (VMSS). Adapts to the user's expertise β beginners get sensible defaults; networking/spec/cost/security experts get the deep questions for their domain only β then emits the chosen artifact: az CLI bash, Bicep, Terraform, or live apply via Azure MCP.
## When to use
- User wants to **create / provision / deploy / spin up** a VM or VMSS (not just pick a SKU)
- User has a recommendation in hand and wants a deployable artifact
- User asks for a "create VM" script, template, or commands in az CLI, Bicep, or Terraform
> **Disambiguator.** If the user wants to deploy an **application** (Docker service, web app, API, function), route to `azure-prepare`. This workflow is for **bare VM/VMSS infrastructure** only.
> **Recommender first.** If the user has not picked a SKU yet ("what should I pick?"), pause and run [vm-recommender](../vm-recommender/vm-recommender.md) Steps 1β6 first, then resume here.
## Workflow
### Step 1 β Determine VM vs VMSS
If the user already said "VM" or "VMSS" / "scale set", use that. Otherwise: autoscaling, multiple identical instances, or stateless tier behind a load balancer β **VMSS**; everything else β **VM**. If unsure, default to single VM and ask one confirmation.
### Step 2 β Depth Probe
Classify the user's first 1β2 messages against the signal table in [depth-probe/index.md](references/depth-probe/index.md) and pick the highest-scoring branch:
| Branch | File |
|---|---|
| Beginner / fast-path | [beginner.md](references/depth-probe/beginner.md) |
| Networking-deep | [networking-deep.md](references/depth-probe/networking-deep.md) |
| Spec-deep | [spec-deep.md](references/depth-probe/spec-deep.md) |
| Cost-deep | [cost-deep.md](references/depth-probe/cost-deep.md) |
| Security-deep | [security-deep.md](references/depth-probe/security-deep.md) |
> Never ask a question whose answer can be inferred or safely defaulted. Batch silent inferences into the Plan Card so the user can see and edit them.
### Step 3 β Adaptive Gather
Ask **only** the questions from the matched branch's matrix. Use the defaults ladder when asking:
> *"NSG inbound rules β `[Recommended: SSH from your IP only]` / `[Show alternatives]` / `[I have specifics]`"*
Cross-branch follow-ups (once, after the primary branch):
- Resource group (existing or new β propose `<vm-name>-rg`)
- Tags (accept "none" without follow-up)
- VMSS only: instance count, orchestration mode (default **Flexible**)
If the user volunteers a deep signal mid-flow, append the relevant matrix questions for that topic. Do not restart.
### Step 4 β Validate
> **GATE β do not present the Plan Card until validation passes.**
Use the Azure MCP read-only tools listed in [validation-gates.md](references/validation-gates.md) (SKU exists in region / image is current / quota headroom / region availability). Required path; CLI fallback is documented in [mcp-tools.md](references/mcp-tools.md).
Outcomes:
| Result | Action |
|---|---|
| β
Sufficient | Proceed to Step 5 |
| β οΈ Near limit (>80%) | Proceed but flag in Plan Card; suggest quota increase |
| β Insufficient / SKU missing | Propose alternate SKU or region; do **not** generate output |
### Step 5 β Plan Card (with explicit-override fast path)
**Default path.** Render a single markdown table summarizing **every decision** (explicit answers + silent defaults). The user reads top-to-bottom and either approves or edits any row before output is generated. See [plan-card.md](references/plan-card.md) for the schema, example, and rendering rules.
Ask: *"Approve as-is, edit a row, or change output format?"* β do not generate until approved.
**Explicit-override fast path.** If the user's prompt combines (a) an explicit deliverable ("give me the Bicep", "just print the az CLI", "apply it via MCP") **and** (b) an explicit refusal of dialog ("no questions", "skip planning", "no plan", "just do it"), **respect them**. Skip the Plan Card table and the approval AskUserQuestion. Instead:
1. Emit a **single-line preview** that surfaces the high-signal decisions inline β e.g. *"β Deploying `Standard_D2s_v5` in `eastus`, OS `Ubuntu2404`, NSG = your public IP only on 22, est. ~$70/mo."*
2. Immediately emit the requested artifact (Bicep / Terraform / az CLI / MCP apply).
3. Mention once, at the end, that the full Plan Card is available on request if they want to edit rows.
Step 4 validation gates (SKU / image / quota / region) still run on the fast path β they protect against broken artifacts, not user intent. If validation fails, fall back to the β outcome in Step 4 (propose alternate SKU/region, do not generate output).
### Step 6 β Output Choice
Ask the user to pick one of four formats (or use the one they already specified):
| Format | When | Adapter |
|---|---|---|
| **az CLI bash** | Quick one-off, learning, copy-paste | [az-cli.md](references/output-adapters/az-cli.md) |
| **Bicep** | Repeatable IaC, production, ARM-native | [bicep.md](references/output-adapters/bicep.md) |
| **Terraform** | Existing TF state, multi-cloud | [terraform.md](references/output-adapters/terraform.md) |
| **Apply via Azure MCP** | "Just do it" β MCP connected, user trusts the Plan Card | [mcp-apply.md](references/output-adapters/mcp-apply.md) |
All four adapters consume the **same Plan Card parameter set** β switching format is a re-render, not a re-gather. For Apply via MCP, confirm one more time (the only destructive path) before calling `compute_vm_create` / `compute_vmss_create`.
### Step 7 β Delivery
> **Skip for Apply via MCP** β the artifact is the live deployment.
For `az CLI` / `Bicep` / `Terraform`, ask one final question: *where should it land?* See [delivery-options/index.md](references/delivery-options/index.md) for the decision logic. Three modes: [print](references/delivery-options/print.md), [save locally](references/delivery-options/save-local.md), [GitHub PR](references/delivery-options/github-pr.md).
If the user later says "also save it locally" or "open the PR now", re-run delivery with the cached Plan Card β **do not re-ask Plan Card questions**.
## Error handling
| Scenario | Action |
|---|---|
| Azure MCP not connected | Skip MCP pre-flight; warn that quota / SKU availability is unverified; offer CLI fallback |
| `compute_vm_list-skus` returns empty | Broaden filter (drop `familyPrefix`, lower `minVCpus`); if still empty, suggest another region |
| Quota insufficient | Show the gap; offer (a) smaller SKU, (b) different family, (c) different region, (d) quota-increase link |
| User wants Windows but supplies SSH key | Switch auth to password (with strength check) or RDP + cert; do not generate broken artifact |
| User asks "what was that az CLI again?" after picking Bicep | Re-render via Adapter 1; do not re-ask questions |
| Custom image / Shared Image Gallery | Pass full resource ID to `compute_vm_list-images`; do not try to map to an alias |
| User requests confidential / FedRAMP / HIPAA controls mid-flow | Append Security-deep questions; flag any defaults that fail the compliance bar |
## Routing back / handoff
| Situation | Route to |
|---|---|
| Deploy an **application** (not a bare VM) | `azure-prepare` skill |
| Reserve capacity *before* creating | [capacity-reservation](../capacity-reservation/capacity-reservation.md) |
| Enroll the new VM in management | [essential-machine-management](../essential-machine-management/essential-machine-management.md) |
| Compare more SKU / pricing options | [vm-recommender](../vm-recommender/vm-recommender.md) Steps 1β6 |
handoff-to-creator.md 1.6 KB
# Hand-off to vm-creator
When the user wants to **provision** the recommended option (not just compare), hand off to [vm-creator](../../vm-creator/vm-creator.md). Don't skip directly to an output adapter β the user must see and approve the Plan Card first.
## Required before hand-off
Render the [Plan Card](../../vm-creator/references/plan-card.md) markdown table **in chat** with the chosen SKU, region, instance count, pricing, and quota status pre-filled from the recommender's work. The user is approving the Plan Card, not the artifact.
## Routing signals
| User says | Action |
|---|---|
| "let's create it" / "spin one up" / "deploy this" | Render Plan Card β route to `vm-creator` Step 5 with selected SKU + region pre-filled |
| "give me the az CLI / Bicep / Terraform" | Render Plan Card β route to `vm-creator` Step 6 (Output Choice) |
| "just compare prices" / "I'm still deciding" | Stay in `vm-recommender`; offer to revisit |
## Example hand-off message
> *"Want me to generate the create command? I can output az CLI, Bicep, Terraform, or apply it via Azure MCP β I'll carry over the SKU, region, and pricing we just landed on."*
## What carries over
| Recommender output | Plan Card row |
|---|---|
| Hosting Model (VM vs VMSS) | `Hosting model` |
| VM Size (ARM SKU) | `Size` |
| Region | `Region` |
| Instance Count (or `minβmax`) | `Instance count` (VMSS only) |
| Estimated $/hr | `Estimated cost` |
| Quota Status (β
/β οΈ/β) | `Quota` |
`vm-creator` Steps 2β4 (Depth Probe, Adaptive Gather, Validate) still run after hand-off to fill in OS, auth, networking, and tagging β they're additive on top of the recommender's spec choice.
web-fetch-policy.md 1.6 KB
# web_fetch policy
Steps 2 and 3 of the recommender rely on `web_fetch` against `learn.microsoft.com` to verify that a recommendation reflects current capabilities (especially VMSS features, family availability, and Spot eligibility).
## When `web_fetch` succeeds
Use the live documentation as the source of truth. Cite the URL in the recommendation so the user can verify.
## When `web_fetch` fails (timeout, 404, blocked, offline)
Proceed using the reference files in `../../references/` β but **always** include this warning in the recommendation:
> β Unable to verify against latest Azure documentation. Recommendation is based on reference material that may not reflect recent updates (e.g., new VM families, Spot eligibility changes, regional rollouts).
Do not block the recommendation on `web_fetch` failure. The user is better served by an annotated recommendation than by no recommendation.
## What to fetch (Step 2 β VMSS)
```
https://learn.microsoft.com/azure/virtual-machine-scale-sets/overview
https://learn.microsoft.com/azure/virtual-machine-scale-sets/virtual-machine-scale-sets-autoscale-overview
```
## What to fetch (Step 3 β VM family)
```
https://learn.microsoft.com/azure/virtual-machines/sizes/<family-category>/<series-name>
```
Examples:
- B-series: `https://learn.microsoft.com/azure/virtual-machines/sizes/general-purpose/b-family`
- D-series: `https://learn.microsoft.com/azure/virtual-machines/sizes/general-purpose/ddsv5-series`
- GPU: `https://learn.microsoft.com/azure/virtual-machines/sizes/gpu-accelerated/nc-family`
For Spot, also: `https://learn.microsoft.com/azure/virtual-machine-scale-sets/use-spot`.
vm-recommender.md 6.4 KB
# Azure VM Recommender
Recommend Azure VM sizes, VM Scale Sets (VMSS), and configurations by analyzing workload type, performance requirements, scaling needs, and budget. No Azure subscription required β data comes from public Microsoft documentation and the unauthenticated Retail Prices API.
## When to Use This Skill
- User asks which Azure VM or VMSS to choose for a workload
- User wants to compare VM families, sizes, or pricing tiers
- User asks about trade-offs (cost vs performance, single VM vs scale set, orchestration modes)
- User needs a cost estimate without an Azure subscription
- User asks "Needs autoscaling?" or wants to decide between a single VM and a scale set
## Workflow
> Use reference files for initial filtering. Then **verify with live documentation** via `web_fetch` before final recommendations. If `web_fetch` fails, fall back to the reference files and surface the staleness warning from [web-fetch-policy.md](references/web-fetch-policy.md).
### Step 1: Gather Requirements
Ask the user (infer when possible):
| Requirement | Examples |
|---|---|
| Workload type | Web server, relational DB, ML training, batch, dev/test |
| vCPU / RAM needs | "4 cores, 16 GB" or "lightweight" / "heavy" |
| GPU needed? | Yes β GPU families; No β general / compute / memory |
| Storage needs | High IOPS, large temp disk, premium SSD |
| Budget priority | Cost-sensitive, performance-first, balanced |
| OS | Linux or Windows (affects pricing) |
| Region | Affects availability and price |
| Instance count | Single, fixed count, or variable |
| Scaling needs | None, manual, autoscale (metrics / schedule) |
| Availability needs | Best-effort, fault-domain, cross-zone HA |
| Load balancing | None, Azure Load Balancer (L4), Application Gateway (L7) |
### Step 2: Determine VM vs VMSS
Review [VMSS Guide](../../references/vmss-guide.md). Decision shortcut β start by asking **Needs autoscaling?** then walk the table:
| Signal | Pick |
|---|---|
| Autoscale on CPU, memory, or schedule | **VMSS** |
| Stateless web/API tier behind a load balancer | **VMSS** |
| Batch / parallel processing across many nodes | **VMSS** |
| Mixed VM sizes in one group | **VMSS (Flexible)** |
| Single long-lived server (jumpbox, AD DC) | **VM** |
| Unique per-instance config | **VM** |
| Stateful, tightly-coupled cluster | **VM** (or VMSS case-by-case) |
If recommending VMSS, verify with `web_fetch` per [web-fetch-policy.md](references/web-fetch-policy.md). When in doubt, default to a single **VM**.
### Step 3: Select VM Family
Review [VM Family Guide](../../references/vm-families.md) and pick 2β3 candidate families. Verify each candidate's specs with `web_fetch` against:
```
https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/<family-category>/<series-name>
```
For Spot eligibility, also fetch `https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/use-spot`. If any fetch fails, follow [web-fetch-policy.md](references/web-fetch-policy.md). Same SKUs apply to single VMs and VMSS.
### Step 4: Look Up Pricing
Query the Azure Retail Prices API per [Retail Prices API Guide](../../references/retail-prices-api.md).
> **VMSS:** no extra charge β pricing is per-VM. Multiply per-instance price Γ expected count. For autoscale, estimate at both `min` and `max`.
### Step 5: Validate Quota Availability
> **GATE β do not present recommendations until quota is validated.**
If the user has a subscription + region, review and run the checks from [VM Quota Validation Guide](../../references/vm-quotas.md). Without a subscription, note quota must be checked before deployment.
| Outcome | Action |
|---|---|
| β
Sufficient | Proceed to Step 6 |
| β οΈ Near limit (>80%) | Proceed but warn; suggest quota increase |
| β Insufficient | Request increase, swap family, or try another region |
Include a "Quota Status" column (β
/β οΈ/β) in the table.
### Step 6: Present Recommendations
Provide **2β3 options** with trade-offs:
| Column | Purpose |
|---|---|
| Hosting Model | VM or VMSS (with orchestration mode if VMSS) |
| VM Size | ARM SKU name (e.g., `Standard_D4s_v5`) |
| vCPUs / RAM | Core specs |
| Instance Count | `1` for VM; `minβmax` for VMSS with autoscale |
| Estimated $/hr | Per-instance pay-as-you-go |
| Why | Workload fit |
| Trade-off | What the user gives up |
Always explain *why* a family fits and the Trade-off (cost vs cores, burstable vs dedicated, VM simplicity vs VMSS scale).
For VMSS, also mention orchestration mode (default **Flexible**), autoscale strategy (metric / schedule / both), and load balancer type.
### Step 7: Offer Next Steps
- Compare reservation / savings plan pricing (`priceType eq 'Reservation'` in the API)
- Suggest [Azure Pricing Calculator](https://azure.microsoft.com/pricing/calculator/) for full estimates
- For VMSS: [autoscale best practices](https://learn.microsoft.com/azure/azure-monitor/autoscale/autoscale-best-practices), [VMSS networking](https://learn.microsoft.com/azure/virtual-machine-scale-sets/virtual-machine-scale-sets-networking)
### Step 8: Hand Off to VM Creator (Optional)
If the user wants to **actually provision** what was recommended, hand off to [vm-creator](../vm-creator/vm-creator.md). See [handoff-to-creator.md](references/handoff-to-creator.md) for the required Plan Card render and routing rules.
## Error Handling
| Scenario | Action |
|---|---|
| API returns empty results | Broaden filters β check `armRegionName`, `serviceName`, `armSkuName` spelling |
| User unsure of workload type | Ask clarifying questions; default to General Purpose D-series |
| Region not specified | Use `eastus` as default; note prices vary by region |
| Unclear if VM or VMSS needed | Ask about scaling + instance count; default to single VM if still unsure |
| User asks VMSS pricing directly | Same VM pricing API; VMSS has no extra charge β multiply by instance count |
## References
- [VM Family Guide](../../references/vm-families.md) β family-to-workload mapping
- [Retail Prices API Guide](../../references/retail-prices-api.md) β query patterns, filters
- [VMSS Guide](../../references/vmss-guide.md) β when to use VMSS, orchestration, autoscale
- [VM Quota Validation Guide](../../references/vm-quotas.md) β vCPU checks, CLI commands
- [web-fetch-policy.md](references/web-fetch-policy.md) β fail-safe behavior for live docs lookups
- [handoff-to-creator.md](references/handoff-to-creator.md) β Step 8 hand-off rules
- [vm-creator](../vm-creator/vm-creator.md) β provision the recommended SKU
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.