Installation

Install with CLI Recommended
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 (50)

SKILL.md 3.2 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, can't connect / RDP / SSH, refused, black screen, reset password, reach VM, port 3389, NSG, security, Linux, troubleshoot, troubleshooting, connectivity, 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.4.3"
---

# 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 **can't connect** to a VM (RDP / SSH / port refused / black screen / password reset)
- 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

```
Azure compute intent?
β”œβ”€β”€ Recommend / compare / price a VM or VMSS         β†’ VM Recommender
β”œβ”€β”€ Create / provision / deploy a VM or VMSS         β†’ VM Creator
β”œβ”€β”€ Can't connect / RDP / SSH / port refused         β†’ VM Troubleshooter
β”œβ”€β”€ Reserve / guarantee capacity (CRG)               β†’ Capacity Reservation
β”œβ”€β”€ Machine enrollment / Essential Machine Management β†’ Essential Machine Management
└── Unclear                                          β†’ Ask which of the above
```

**Routing rule:** read the matched workflow file before any reference file. The workflow owns the step-by-step guidance; references are looked up on demand.

## Workflows

| Workflow | File | Use when |
|---|---|---|
| **VM Recommender** | [vm-recommender.md](workflows/vm-recommender/vm-recommender.md) | User asks which VM/VMSS to choose, wants pricing, or wants to compare options |
| **VM Creator** | [vm-creator.md](workflows/vm-creator/vm-creator.md) | User wants to provision a bare VM or VMSS (not an app deployment) |
| **VM Troubleshooter** | [vm-troubleshooter.md](workflows/vm-troubleshooter/vm-troubleshooter.md) | User can't connect, RDP/SSH refused, black screen, needs password reset |
| **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 |
references/
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)
workflows/capacity-reservation/
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                                                  |
workflows/capacity-reservation/references/
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)
workflows/essential-machine-management/
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 |
workflows/essential-machine-management/references/
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
workflows/vm-creator/examples/bicep/
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
workflows/vm-creator/examples/terraform/
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
}
workflows/vm-creator/references/delivery-options/
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.
workflows/vm-creator/references/depth-probe/
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.
workflows/vm-creator/references/
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.
workflows/vm-creator/references/output-adapters/
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`.
workflows/vm-creator/
vm-creator.md 7.5 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 |
| Post-create RDP / SSH issues | [vm-troubleshooter](../vm-troubleshooter/vm-troubleshooter.md) |
workflows/vm-recommender/references/
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`.
workflows/vm-recommender/
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
workflows/vm-troubleshooter/references/
cannot-connect-to-vm.md 5.6 KB
# Cannot Connect to VM

Index of VM connectivity troubleshooting references. Route to the appropriate file based on the user's symptom category.

> ⚠️ **Determine OS first.** If the user hasn't stated their OS, check via CLI (`az vm get-instance-view`) or ask. OS matters because:
> - **Windows** β†’ RDP (port 3389), Windows Firewall, TermService, PowerShell Run Commands
> - **Linux** β†’ SSH (port 22), iptables/firewalld/UFW, sshd, Shell Run Commands
> - **Other images** (FreeBSD, Flatcar, etc.) β†’ SSH; firewall and init systems vary β€” fetch the latest docs

## Routing

| Signal in User Message                                                         | Category                  | Reference                                                  |
| ------------------------------------------------------------------------------ | ------------------------- | ---------------------------------------------------------- |
| "can't RDP", timeout, black screen, RDP error, internal error                  | Unable to RDP             | [rdp-connectivity.md](rdp-connectivity.md)                 |
| "can't SSH", refused, permission denied, publickey                             | Unable to SSH             | [ssh-connectivity.md](ssh-connectivity.md)                 |
| NSG, no public IP, NIC disabled, routing, effective rules                      | Network Issues            | [network-connectivity.md](network-connectivity.md)         |
| Guest firewall, Windows Firewall, iptables, firewalld, BlockInboundAlways      | Firewall Blocking         | [firewall-blocking.md](firewall-blocking.md)               |
| VM agent down, Run Command timeout, Serial Console, boot diagnostics, BSOD     | VM Agent Not Responding   | [vm-agent-not-responding.md](vm-agent-not-responding.md)   |
| Wrong password, credentials, access denied, CredSSP, account expired           | Credential / Auth Errors  | [credential-auth-errors.md](credential-auth-errors.md)     |
| TermService stopped, RDP disabled, port changed, TLS cert, NLA, GPO, licensing | RDP Service / Config      | [rdp-service-config.md](rdp-service-config.md)             |

## Workflow

1. Identify the symptom category from the routing table above
2. Open the matching reference file for the Symptoms β†’ Solutions table and Quick Commands
3. Narrow to the specific solution row matching the user's symptom
4. **Before any extension-backed operation, run [Pre-Flight Safety Checks](#pre-flight-safety-checks)**
5. Fetch the linked documentation URL for the latest guidance
6. Summarize diagnostic steps and resolution, referencing the official docs

---

## Pre-Flight Safety Checks

> ⚠️ **Warning:** Always run these checks before any command that depends on the VM agent or extensions (`az vm user update`, `az vm user reset-ssh`, `az vm user reset-remote-desktop`, `az vm run-command invoke`). Running extension-backed operations on a VM with an unhealthy agent or stuck extensions can **deadlock the VM** and require manual portal recovery.

```bash
# 1. Check VM power state, provisioning state, and agent status
az vm get-instance-view --name <vm-name> -g <resource-group> \
  --query "instanceView.{powerState:[statuses[?starts_with(code,'PowerState/')]][0][0].code, provisioningState:[statuses[?starts_with(code,'ProvisioningState/')]][0][0].code, vmAgentStatus:vmAgent.statuses[0].displayStatus}" -o json

# 2. Check existing extension states
az vm extension list --vm-name <vm-name> -g <resource-group> \
  --query "[].{name:name, provisioningState:provisioningState}" -o table
```

| Check | Safe Value | Unsafe β€” Do NOT proceed |
| ----- | ---------- | ----------------------- |
| Power state | `PowerState/running` | Any other value, missing, or query error |
| Provisioning state | `ProvisioningState/succeeded` | `Updating`, `Creating`, `Failed`, `Deleting`, missing, or query error |
| VM agent status | `Ready` | `Not Ready`, `null`, missing, or query error |
| Extension states | All `Succeeded` or no extensions | Any extension in `Creating`, `Updating`, `Deleting`, or `Failed` |

> πŸ’‘ **Tip:** If a check returns `null`, empty, or the CLI command itself errors, treat the result as **unsafe**.

**If any check is unsafe:**
1. **Stop.** Do NOT run any extension-backed command.
2. Inform the user which check(s) failed and what the current state is.
3. Use non-agent alternatives: **Serial Console**, **offline repair VM**, or **Portal-based actions**.
4. If the state appears transient (e.g., VM just started, provisioning briefly not `succeeded`), wait 30–60 seconds and **re-run the checks only** β€” do not run the extension command until all checks pass.

---

## Escalation

If the issue doesn't match any symptom above, or if the documented solutions don't resolve it:

1. **Check Azure Resource Health** β€” Portal > VM > Resource health (checks for platform-level issues)
2. **Offer to restart the VM** (requires user approval) β€” `az vm restart --name <vm-name> -g <resource-group>`
3. **Offer to redeploy the VM** (requires user approval β€” moves to new host) β€” `az vm redeploy --name <vm-name> -g <resource-group>`
4. **Comprehensive troubleshooting:**
   - Windows: [Troubleshoot RDP connections](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-rdp-connection)
   - Linux: [Troubleshoot SSH connections](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/troubleshoot-ssh-connection)
   - Windows hub: [All Windows VM troubleshooting docs](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/welcome-virtual-machines-windows)
   - Linux hub: [All Linux VM troubleshooting docs](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/welcome-virtual-machines-linux)
credential-auth-errors.md 5.7 KB
# Credential and Authentication Errors

User can reach the VM but authentication fails.

## Windows (RDP) β€” Symptoms β†’ Solutions

| Symptom                                                    | Solution                                                                      | Documentation                                                                                                                                 |
| ---------------------------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| "Your credentials did not work"                            | Reset password via Portal or CLI                                              | [Reset RDP service or password](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/reset-rdp)                      |
| "Must change password before logging on"                   | Reset password via Portal (bypasses the requirement)                          | [Reset RDP service or password](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/reset-rdp)                      |
| "This user account has expired"                            | Extend account via Run Command: `net user <user> /expires:never`              | [Reset RDP service or password](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/reset-rdp)                      |
| "Trust relationship between workstation and domain failed" | Reset machine account or rejoin domain                                        | [Troubleshoot RDP connection](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-rdp-connection)      |
| "Access is denied" / "Connection was denied"               | Add user to Remote Desktop Users group                                        | [Specific RDP errors](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-specific-rdp-errors#wincred) |
| Wrong username format                                      | Use `VMNAME\user` for local, `DOMAIN\user` for domain accounts                | [Specific RDP errors](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-specific-rdp-errors#wincred) |
| CredSSP "encryption oracle" error                          | Temporary: set AllowEncryptionOracle=2 on client; permanent: patch both sides | [CredSSP remediation](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/credssp-encryption-oracle-remediation)    |

## Linux (SSH) β€” Symptoms β†’ Solutions

| Symptom                                                     | Solution                                                                      | Documentation                                                                                                                                    |
| ----------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| "Permission denied (publickey)"                             | Wrong key, wrong user, or key not in authorized_keys β€” reset key via CLI      | [Detailed SSH troubleshooting](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/detailed-troubleshoot-ssh-connection) |
| "Permission denied (password)"                              | Wrong password or password auth disabled in sshd_config                        | [Detailed SSH troubleshooting](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/detailed-troubleshoot-ssh-connection) |
| Account locked after failed attempts                        | Unlock via Run Command: `passwd -u <user>` or `pam_tally2 --reset --user <user>` | [Troubleshoot SSH connection](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/troubleshoot-ssh-connection)           |
| "Permission denied" with Entra ID (AAD) SSH login           | Missing role: Virtual Machine Administrator Login or Virtual Machine User Login | [Troubleshoot SSH connection](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/troubleshoot-ssh-connection)           |
| sudo password prompt fails / user not in sudoers            | Fix via Run Command or Serial Console                                          | [Troubleshoot SSH connection](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/troubleshoot-ssh-connection)           |

## Quick Commands β€” Windows

> ⚠️ **Warning:** Commands below use the VM agent/extensions. Run [Pre-Flight Safety Checks](cannot-connect-to-vm.md#pre-flight-safety-checks) before using them.

```bash
# ⚑ Reset password
az vm user update --name <vm-name> -g <resource-group> -u <username> -p '<new-password>'

# ⚑ Reset RDP configuration (also re-enables NLA)
az vm user reset-remote-desktop --name <vm-name> -g <resource-group>
```

## Quick Commands β€” Linux

> ⚠️ **Warning:** Commands below use the VM agent/extensions. Run [Pre-Flight Safety Checks](cannot-connect-to-vm.md#pre-flight-safety-checks) before using them.

```bash
# ⚑ Reset SSH public key
az vm user update --name <vm-name> -g <resource-group> \
  -u <username> --ssh-key-value "<ssh-public-key>"

# ⚑ Reset password for Linux VM
az vm user update --name <vm-name> -g <resource-group> \
  -u <username> -p '<new-password>'

# ⚑ Unlock a locked account via Run Command
az vm run-command invoke --name <vm-name> -g <resource-group> \
  --command-id RunShellScript --scripts "passwd -u <username>"
```
firewall-blocking.md 4.8 KB
# Firewall Blocking Connectivity

Guest OS firewall (Windows Firewall or Linux iptables/firewalld) is blocking inbound connections even though NSG allows them.

## Symptoms β†’ Solutions

| Symptom                                                     | OS      | Solution                                             | Documentation                                                                                                                                          |
| ----------------------------------------------------------- | ------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Windows Firewall blocking RDP                               | Windows | Re-enable "Remote Desktop" firewall rule group       | [Guest OS firewall blocking](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/guest-os-firewall-blocking-inbound-traffic) |
| Firewall policy set to BlockInboundAlways                   | Windows | Reset to `blockinbound,allowoutbound` policy         | [Enable/disable firewall rule](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/enable-disable-firewall-rule-guest-os)    |
| Third-party AV/firewall blocking                            | Windows | Stop the third-party service, test, then reconfigure | [Guest OS firewall blocking](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/guest-os-firewall-blocking-inbound-traffic) |
| iptables/nftables blocking SSH (port 22)                    | Linux   | Add allow rule or flush blocking chain               | [Troubleshoot SSH connection](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/troubleshoot-ssh-connection)                 |
| firewalld blocking SSH                                      | Linux   | Open port 22 in the active zone                      | [Troubleshoot SSH connection](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/troubleshoot-ssh-connection)                 |
| UFW blocking SSH (Ubuntu/Debian)                            | Linux   | Run `ufw allow 22/tcp` or disable UFW temporarily    | [Troubleshoot SSH connection](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/troubleshoot-ssh-connection)                 |
| Cannot access firewall settings β€” no connectivity (Windows) | Windows | Use offline repair VM to modify registry             | [Disable guest OS firewall offline](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/disable-guest-os-firewall-windows)   |
| Cannot access firewall settings β€” no connectivity (Linux)   | Linux   | Use Serial Console or repair VM to edit iptables/firewalld config | [Repair Linux VM commands](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/repair-linux-vm-using-azure-virtual-machine-repair-commands) |

## Quick Commands β€” Windows

> ⚠️ **Warning:** Commands marked with ⚑ use the VM agent/extensions. Run [Pre-Flight Safety Checks](cannot-connect-to-vm.md#pre-flight-safety-checks) before using them.

```bash
# ⚑ Reset RDP config (re-enables RDP, creates firewall rule for 3389)
az vm user reset-remote-desktop --name <vm-name> -g <resource-group>

# ⚑ Query Windows Firewall rules via Run Command
az vm run-command invoke --name <vm-name> -g <resource-group> \
  --command-id RunPowerShellScript \
  --scripts "netsh advfirewall firewall show rule name='Remote Desktop - User Mode (TCP-In)'"

# ⚑ Enable Remote Desktop firewall rule via Run Command
az vm run-command invoke --name <vm-name> -g <resource-group> \
  --command-id RunPowerShellScript \
  --scripts "netsh advfirewall firewall set rule group='Remote Desktop' new enable=yes"
```

## Quick Commands β€” Linux

> ⚠️ **Warning:** Commands below use the VM agent/extensions. Run [Pre-Flight Safety Checks](cannot-connect-to-vm.md#pre-flight-safety-checks) before using them.

```bash
# ⚑ Check iptables rules via Run Command
az vm run-command invoke --name <vm-name> -g <resource-group> \
  --command-id RunShellScript --scripts "iptables -L -n --line-numbers"

# ⚑ Allow SSH through iptables via Run Command
az vm run-command invoke --name <vm-name> -g <resource-group> \
  --command-id RunShellScript --scripts "iptables -I INPUT -p tcp --dport 22 -j ACCEPT"

# ⚑ Check firewalld status and open SSH via Run Command
az vm run-command invoke --name <vm-name> -g <resource-group> \
  --command-id RunShellScript \
  --scripts "firewall-cmd --state && firewall-cmd --add-service=ssh --permanent && firewall-cmd --reload"

# Check/allow UFW (Ubuntu/Debian) via Run Command
az vm run-command invoke --name <vm-name> -g <resource-group> \
  --command-id RunShellScript --scripts "ufw status; ufw allow 22/tcp"
```
network-connectivity.md 4.7 KB
# Network Connectivity Problems

User's VM is running but unreachable due to network-level issues (NSG, routing, NIC, DNS).

> ⚠️ **OS Note:** NSG, routing, and public IP issues are OS-agnostic (Azure platform layer). NIC and DNS issues have OS-specific remediation β€” see the OS column below.

## Symptoms β†’ Solutions

| Symptom                                    | OS      | Solution                                                | Documentation                                                                                                                                  |
| ------------------------------------------ | ------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| NSG has no allow rule for RDP/SSH port     | Any     | Add inbound allow rule for TCP 3389 or 22               | [NSG blocking RDP](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-rdp-nsg-problem)                 |
| NSG at both NIC and subnet level blocking  | Any     | Traffic must pass both NSGs β€” check effective rules     | [Diagnose VM traffic filtering](https://learn.microsoft.com/en-us/azure/network-watcher/diagnose-vm-network-traffic-filtering-problem)         |
| Custom route (UDR) sending traffic to NVA  | Any     | Check effective routes, verify NVA is forwarding        | [Diagnose VM routing](https://learn.microsoft.com/en-us/azure/network-watcher/diagnose-vm-network-routing-problem)                             |
| VM has no public IP                        | Any     | Add a public IP or connect via Azure Bastion            | [Public IP addresses](https://learn.microsoft.com/en-us/azure/virtual-network/ip-services/public-ip-addresses)                                 |
| NIC is disabled inside guest OS            | Windows | Enable NIC via Run Command or Serial Console            | [Troubleshoot RDP β€” NIC disabled](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-rdp-nic-disabled) |
| NIC is down inside guest OS                | Linux   | Bring interface up via Run Command or Serial Console    | [Troubleshoot SSH connection](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/troubleshoot-ssh-connection)          |
| Static IP misconfiguration inside guest    | Windows | Azure VMs should use DHCP; reset NIC to restore         | [Reset network interface](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/reset-network-interface)               |
| Static IP misconfiguration inside guest    | Linux   | Restore DHCP config in `/etc/netplan/` or `/etc/sysconfig/network-scripts/` | [Troubleshoot SSH connection](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/troubleshoot-ssh-connection) |
| Ghost NIC after disk swap or resize        | Windows | Old NIC holds IP config, new NIC can't get IP           | [Reset network interface](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/reset-network-interface)               |
| DNS resolution failure                     | Any     | Check DNS server config; Azure default is 168.63.129.16 | [DHCP troubleshooting](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-rdp-dhcp-disabled)           |

## Quick Commands β€” Platform (Any OS)

```bash
# Check effective NSG rules on a NIC
az network nic list-effective-nsg --name <nic-name> -g <resource-group>

# Check effective routes
az network nic show-effective-route-table --name <nic-name> -g <resource-group> -o table

# Check if VM has a public IP
az vm list-ip-addresses --name <vm-name> -g <resource-group> -o table

# Test connectivity from VM to a destination
az network watcher test-connectivity --source-resource <vm-resource-id> \
  --dest-address <destination-ip> --dest-port <port> -g <resource-group>
```

## Quick Commands β€” Windows

```bash
# Reset NIC (restores DHCP, removes stale config β€” Windows only)
az vm repair reset-nic --name <vm-name> -g <resource-group> --yes
```

## Quick Commands β€” Linux

> ⚠️ **Warning:** Commands below use the VM agent/extensions. Run [Pre-Flight Safety Checks](cannot-connect-to-vm.md#pre-flight-safety-checks) before using them.

```bash
# ⚑ Check network interface status via Run Command
az vm run-command invoke --name <vm-name> -g <resource-group> \
  --command-id RunShellScript --scripts "ip link show; ip addr show"

# ⚑ Bring interface up via Run Command
az vm run-command invoke --name <vm-name> -g <resource-group> \
  --command-id RunShellScript --scripts "ip link set eth0 up && dhclient eth0"
```
rdp-connectivity.md 6.9 KB
# Unable to RDP into the VM

User is trying to RDP into a Windows VM but the connection fails (timeout, refused, or error dialog).

## Symptoms β†’ Solutions

| Symptom                                                                     | Solution                                                   | Documentation                                                                                                                                                                            |
| --------------------------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Connection times out, no response at all                                    | NSG missing allow rule for port 3389                       | [NSG blocking RDP](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-rdp-nsg-problem)                                                           |
| Connection times out, NSG rules look correct                                | Guest OS firewall is blocking inbound RDP                  | [Guest OS firewall blocking](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/guest-os-firewall-blocking-inbound-traffic)                                   |
| "Your credentials did not work"                                             | Wrong password or username format                          | [Credentials error](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-specific-rdp-errors#windows-security-error-your-credentials-did-not-work) |
| "An internal error has occurred"                                            | RDP service, TLS certificate, or security layer issue      | [RDP internal error](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-rdp-internal-error)                                                      |
| Black screen after login                                                    | Explorer.exe crash, GPU driver, or GPO stuck               | [Detailed RDP troubleshooting](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/detailed-troubleshoot-rdp)                                                  |
| "No Remote Desktop License Servers available"                               | RDS licensing grace period expired                         | [Specific RDP errors](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-specific-rdp-errors#rdplicense)                                         |
| "Remote Desktop can't find the computer"                                    | VM has no public IP, DNS issue, or VM is deallocated       | [Specific RDP errors](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-specific-rdp-errors#rdpname)                                            |
| "An authentication error has occurred / LSA"                                | NLA/CredSSP mismatch, clock skew, or wrong username format | [Specific RDP errors](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-specific-rdp-errors#rdpauth)                                            |
| "Remote Desktop can't connect to the remote computer"                       | Generic β€” multiple possible causes                         | [Specific RDP errors](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-specific-rdp-errors#rdpconnect)                                         |
| "Because of a security error"                                               | TLS certificate or version mismatch                        | [RDP general error](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-rdp-general-error)                                                        |
| RDP connects then disconnects immediately                                   | Session limits, idle timeout, or resource exhaustion       | [RDP disconnections](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-rdp-connection)                                                          |
| Works from some IPs but not others                                          | NSG source IP restriction too narrow                       | [NSG blocking RDP](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-rdp-nsg-problem)                                                           |
| Event log shows specific RDP error Event IDs                                | Match Event ID to known cause (e.g., 1058, 36870)          | [RDP issues by Event ID](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/event-id-troubleshoot-vm-rdp-connecton)                                           |
| "Authentication error has occurred" / "function requested is not supported" | CredSSP, NLA, or certificate issue                         | [Authentication errors on RDP](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/cannot-connect-rdp-azure-vm)                                                |
| Guest NIC is disabled inside the VM                                         | Enable NIC via Run Command or Serial Console               | [Troubleshoot RDP β€” NIC disabled](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-rdp-nic-disabled)                                           |

## Quick Commands

> ⚠️ **Warning:** Commands marked with ⚑ use the VM agent/extensions. Run [Pre-Flight Safety Checks](cannot-connect-to-vm.md#pre-flight-safety-checks) before using them.

```bash
# Check VM power state
az vm get-instance-view --name <vm-name> -g <resource-group> \
  --query "instanceView.statuses[1].displayStatus" -o tsv

# Check NSG rules
az network nsg rule list --nsg-name <nsg-name> -g <resource-group> -o table

# ⚑ Reset RDP configuration to defaults (re-enables RDP, resets port, restarts TermService)
az vm user reset-remote-desktop --name <vm-name> -g <resource-group>

# ⚑ Reset VM password
az vm user update --name <vm-name> -g <resource-group> -u <username> -p '<new-password>'

# IP Flow Verify β€” test if NSG allows traffic
az network watcher test-ip-flow --direction Inbound --protocol TCP \
  --local <vm-private-ip>:3389 --remote <your-public-ip>:* \
  --vm <vm-name> -g <resource-group>
```

## General RDP Troubleshooting

If the symptom doesn't match a specific row above, follow Microsoft's systematic approach:

- [Troubleshoot RDP connections to an Azure VM](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-rdp-connection)
- [Detailed RDP troubleshooting steps](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/detailed-troubleshoot-rdp)
rdp-service-config.md 3.2 KB
# RDP Service and Configuration Issues

VM is reachable but the RDP service itself is broken or misconfigured.

## Symptoms β†’ Solutions

| Symptom                                | Solution                                                               | Documentation                                                                                                                                    |
| -------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| TermService not running                | Start the service and set to Automatic                                 | [Reset RDP service](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/reset-rdp)                                     |
| RDP port changed from 3389             | Reset port or update NSG to allow the custom port                      | [Detailed RDP troubleshooting](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/detailed-troubleshoot-rdp)          |
| RDP disabled (fDenyTSConnections = 1)  | Reset RDP config via CLI or Portal                                     | [Reset RDP service](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/reset-rdp)                                     |
| TLS/SSL certificate expired or corrupt | Delete cert and restart TermService to regenerate                      | [RDP internal error](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-rdp-internal-error)              |
| NLA/Security Layer mismatch            | Temporarily disable NLA for recovery                                   | [RDP general error](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-rdp-general-error)                |
| GPO overriding local RDP settings      | Check `HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services` | [Detailed RDP troubleshooting](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/detailed-troubleshoot-rdp)          |
| RDS licensing expired                  | Remove RDSH role or configure license server                           | [Specific RDP errors](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-specific-rdp-errors#rdplicense) |

## Quick Commands

> ⚠️ **Warning:** Commands marked with ⚑ use the VM agent/extensions. Run [Pre-Flight Safety Checks](cannot-connect-to-vm.md#pre-flight-safety-checks) before using them.

```bash
# ⚑ Reset all RDP configuration to defaults
az vm user reset-remote-desktop --name <vm-name> -g <resource-group>

# ⚑ Check TermService status via Run Command
az vm run-command invoke --name <vm-name> -g <resource-group> \
  --command-id RunPowerShellScript --scripts "Get-Service TermService | Select-Object Status, StartType"

# Restart VM (if RDP service is unrecoverable β€” requires user approval)
az vm restart --name <vm-name> -g <resource-group>

# Redeploy VM (moves to new host β€” last resort, requires user approval)
az vm redeploy --name <vm-name> -g <resource-group>
```
ssh-connectivity.md 5.4 KB
# Unable to SSH into the VM

User is trying to SSH into a Linux VM but the connection fails.

## Symptoms β†’ Solutions

| Symptom                                           | Solution                                                                                   | Documentation                                                                                                                                    |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| "Connection refused" on port 22                   | SSH service not running or listening on a different port                                   | [Troubleshoot SSH connection](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/troubleshoot-ssh-connection)           |
| "Connection timed out"                            | NSG blocking port 22, VM not running, or no public IP                                      | [Troubleshoot SSH connection](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/troubleshoot-ssh-connection)           |
| "Permission denied (publickey)"                   | Wrong SSH key, wrong user, or key not in authorized_keys                                   | [Detailed SSH troubleshooting](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/detailed-troubleshoot-ssh-connection) |
| "Permission denied (password)"                    | Wrong password or password auth disabled in sshd_config                                    | [Detailed SSH troubleshooting](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/detailed-troubleshoot-ssh-connection) |
| "Host key verification failed"                    | VM was redeployed and got a new host key                                                   | Remove old entry from `~/.ssh/known_hosts`                                                                                                       |
| "Server unexpectedly closed connection"           | Disk full, SSH config error, or PAM issue                                                  | [Detailed SSH troubleshooting](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/detailed-troubleshoot-ssh-connection) |
| SSH hangs with no response                        | Firewall (iptables/firewalld), routing, or NIC issue                                       | [Troubleshoot SSH connection](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/troubleshoot-ssh-connection)           |
| Cannot SSH into Debian Linux VM                   | Debian-specific network or sshd config issue                                               | [Cannot connect to Debian Linux VM](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/cannot-connect-debian-linux)     |
| SSH blocked after SELinux policy change           | SELinux misconfigured β€” blocking sshd                                                      | [SELinux troubleshooting](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/linux-selinux-troubleshooting)             |
| "Permission denied" with Entra ID (AAD) SSH login | Missing role assignment: Virtual Machine Administrator Login or Virtual Machine User Login | [Troubleshoot SSH connection](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/troubleshoot-ssh-connection)           |
| Linux VM not booting β€” UEFI boot failure          | Gen2 VM UEFI boot issue preventing SSH                                                     | [Linux VM UEFI boot failures](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/azure-linux-vm-uefi-boot-failures)     |

## Quick Commands

> ⚠️ **Warning:** Commands marked with ⚑ use the VM agent/extensions. Run [Pre-Flight Safety Checks](cannot-connect-to-vm.md#pre-flight-safety-checks) before using them.

```bash
# ⚑ Reset SSH configuration to defaults (resets sshd_config, restarts sshd)
az vm user reset-ssh --name <vm-name> -g <resource-group>

# ⚑ Reset SSH public key for a user
az vm user update --name <vm-name> -g <resource-group> \
  -u <username> --ssh-key-value "<ssh-public-key>"

# ⚑ Reset password for Linux VM
az vm user update --name <vm-name> -g <resource-group> \
  -u <username> -p '<new-password>'

# ⚑ Check if sshd is running via Run Command
az vm run-command invoke --name <vm-name> -g <resource-group> \
  --command-id RunShellScript --scripts "systemctl status sshd"

# ⚑ Check SELinux status via Run Command
az vm run-command invoke --name <vm-name> -g <resource-group> \
  --command-id RunShellScript --scripts "getenforce"

# ⚑ Set SELinux to permissive mode (temporary β€” survives until reboot)
az vm run-command invoke --name <vm-name> -g <resource-group> \
  --command-id RunShellScript --scripts "setenforce 0"
```

## General SSH Troubleshooting

If the symptom doesn't match a specific row above, follow Microsoft's systematic approach:

- [Troubleshoot SSH connections to an Azure Linux VM](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/troubleshoot-ssh-connection)
- [Detailed SSH troubleshooting steps](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/detailed-troubleshoot-ssh-connection)
vm-agent-not-responding.md 3.6 KB
# VM Agent Not Responding

Run Command and password reset depend on the VM agent. If the agent is unhealthy, alternative methods are needed.

> ⚠️ **OS Note:** Serial Console, Boot Diagnostics, and repair VM commands are available for both Windows and Linux but use separate doc pages and tools. Match the correct OS below.

## Symptoms β†’ Solutions

| Symptom                                                          | OS      | Solution                                                 | Documentation                                                                                                                                                       |
| ---------------------------------------------------------------- | ------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Run Command times out                                            | Windows | VM agent may be down β€” use Serial Console instead        | [Serial Console β€” Windows](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/serial-console-overview)                                   |
| Run Command times out                                            | Linux   | VM agent may be down β€” use Serial Console instead        | [Serial Console β€” Linux](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/serial-console-linux)                                         |
| Password reset fails via Portal/CLI                              | Windows | VMAccess extension can't communicate β€” use offline reset | [Reset password without agent](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/reset-local-password-without-agent)                    |
| Password/key reset fails via Portal/CLI                          | Linux   | VMAccess extension can't communicate β€” use Serial Console | [Serial Console β€” Linux](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/serial-console-linux)                                        |
| VM not booting (Boot Diagnostics shows BSOD/stuck)               | Windows | OS-level issue β€” use repair VM for offline fix           | [Repair Windows VM](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/repair-windows-vm-using-azure-virtual-machine-repair-commands)    |
| VM not booting (Boot Diagnostics shows kernel panic/stuck)       | Linux   | Use repair VM for offline Linux disk fix                 | [Repair Linux VM](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/repair-linux-vm-using-azure-virtual-machine-repair-commands)          |
| VMAccess extension error on domain controller                    | Windows | VMAccess doesn't support DCs β€” use Serial Console        | [Serial Console β€” Windows](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/serial-console-overview)                                   |

## Quick Commands

```bash
# Connect to Serial Console via CLI
az serial-console connect --name <vm-name> -g <resource-group>

# Enable boot diagnostics (required for Serial Console)
az vm boot-diagnostics enable --name <vm-name> -g <resource-group>

# Get boot diagnostics screenshot/log
az vm boot-diagnostics get-boot-log --name <vm-name> -g <resource-group>

# Create repair VM for offline fixes
az vm repair create --name <vm-name> -g <resource-group> \
  --repair-username repairadmin --repair-password '<password>'

# Restore after offline fix
az vm repair restore --name <vm-name> -g <resource-group>
```
workflows/vm-troubleshooter/
vm-troubleshooter.md 9.8 KB
# Azure VM Connectivity Troubleshooting

> Diagnose and resolve Azure VM connectivity failures (RDP/SSH) by identifying symptoms, routing to the right solution, fetching the latest Microsoft documentation, and guiding the user through resolution.

## Quick Reference

| Property      | Details                                                                                                           |
| ------------- | ----------------------------------------------------------------------------------------------------------------- |
| Best for      | RDP/SSH connection failures, NSG/firewall misconfig, credential resets, NIC issues                                |
| Primary tools | Azure CLI, Azure PowerShell, Serial Console, Boot Diagnostics, Run Command                                        |
| Reference     | [references/cannot-connect-to-vm.md](references/cannot-connect-to-vm.md) |

## MCP Tools

| Tool            | Purpose                                                | Parameters                                                                                                           |
| --------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| `fetch_webpage` | Fetch latest Microsoft troubleshooting docs at runtime | `urls` (Required): Array of doc URLs from reference file; `query` (Optional): User's symptom for relevant extraction |

## Triggers

Activate this skill when user mentions:

- "can't connect to my VM" / "can't RDP" / "can't SSH"
- "RDP not working" / "SSH refused" / "connection timed out"
- "black screen" on VM
- "reset VM password" / "forgot password"
- "NSG blocking" / "firewall blocking" / "port 3389"
- "serial console" access
- "internal error" on RDP
- "VM not reachable" / "public IP not working"
- "RDP disconnects" / "session dropped"

---

## Guardrails

- **Default to read-only diagnostics.** Gather evidence before suggesting any fix.
- Do not run extension-backed commands (`az vm user update`, `az vm user reset-ssh`, `az vm user reset-remote-desktop`, `az vm run-command invoke`) without first passing [Pre-Flight Safety Checks](#phase-25-pre-flight-safety-checks-before-extension-backed-operations).
- Do not restart, redeploy, deallocate, or delete a VM unless the user explicitly asks for remediation.
- Do not conclude root cause without quoting the evidence that supports it (e.g., NSG rule output, VM agent status, extension state).
- When multiple issues are found (e.g., NSG + credential), fix the network-layer issue first before attempting agent-dependent fixes.

## Evidence Order

Gather diagnostic evidence in this order before suggesting remediation:

1. **VM state:** power state, provisioning state, VM agent health, extension states
2. **Network layer:** public IP, NSG rules (NIC + subnet), effective routes, IP flow verify
3. **Guest OS layer (if agent is healthy):** service status via Run Command, firewall rules, sshd/TermService config

---

## Workflow

### Phase 1: Determine User Intent

Infer the connectivity issue from the user's message. If the issue is clear, proceed to Phase 2. If ambiguous, ask **one** clarifying question:

| Signal in User Message                                                    | Inferred Category  |
| ------------------------------------------------------------------------- | ------------------ |
| "can't RDP", "RDP timeout", "RDP error", "black screen", "internal error" | Unable to RDP      |
| "can't SSH", "SSH refused", "permission denied", "publickey"              | Unable to SSH      |
| "NSG", "firewall", "port blocked", "no public IP", "NIC disabled"         | Network / Firewall |
| "credentials", "password", "wrong password", "access denied"              | Credential / Auth  |
| "VM agent", "Run Command not working", "Serial Console"                   | VM Agent / Tools   |

If unclear, ask: **"Are you trying to connect via RDP (Windows) or SSH (Linux), and what error message or behavior are you seeing?"**

If the user shares an Azure VM name or resource ID, attempt to use the azure-resource-lookup skill if available. If not available, attempt to use the Azure CLI.

### Phase 2: Route to Solution

Open [references/cannot-connect-to-vm.md](references/cannot-connect-to-vm.md) and use its routing table to identify the symptom category and open the matching sub-reference for the full **Symptoms β†’ Solutions** table and Quick Commands.

If additional details are needed to narrow to a specific solution row, ask the user. For example:
- "What error message do you see in the RDP dialog?"
- "Does the connection time out, or do you get an error immediately?"
- "Is this a Windows or Linux VM?"

### Phase 2.5: Pre-Flight Safety Checks (Before Extension-Backed Operations)

> ⚠️ **Warning:** This phase is **mandatory** before running any command that depends on the VM agent or extensions. Skipping these checks can deadlock the VM and require manual portal recovery.

**Extension-backed commands include:** `az vm user update`, `az vm user reset-ssh`, `az vm user reset-remote-desktop`, `az vm run-command invoke`, and any operation that installs or invokes a VM extension.

Run the pre-flight checks from [references/cannot-connect-to-vm.md β€” Pre-Flight Safety Checks](references/cannot-connect-to-vm.md#pre-flight-safety-checks) and evaluate:

| Check | Required Value | If Failed |
| ----- | -------------- | --------- |
| VM power state | `PowerState/running` | Start the VM first |
| VM provisioning state | `ProvisioningState/succeeded` | Do NOT run extension commands. Wait for current operation to complete, or use Serial Console / offline repair |
| VM agent status | `Ready` | Do NOT run extension commands. Use Serial Console or offline repair instead |
| Existing extensions | No extensions in `Creating`, `Updating`, or `Deleting` state | Do NOT add new extensions. Wait for completion, remove stuck extensions via Portal, or use Serial Console |

> πŸ’‘ **Tip:** If any check returns `null`, empty, or the CLI command itself errors, treat the result as **unsafe**.

**If any check fails:**
1. **Stop.** Do NOT attempt any extension-backed remediation.
2. **Inform the user** which check(s) failed and what the current state is.
3. **Suggest non-agent alternatives:** Serial Console, offline repair VM, or Portal-based actions.
4. If the state appears transient (e.g., VM just started), wait 30–60 seconds and **re-run the pre-flight checks** β€” do not run the extension command until all checks pass.

### Phase 3: Fetch Documentation

Once you've identified the specific solution row, fetch the linked Microsoft documentation URL for the latest troubleshooting guidance:

```javascript
fetch_webpage({
  urls: ["<documentation-url-from-solution-row>"],
  query: "<user's specific symptom or error message>"
})
```

This ensures the user gets current guidance even if Microsoft updates their docs.

### Phase 4: Diagnose and Respond

Combine the fetched documentation with the quick commands from the reference file to give the user a response:

1. **Explain the likely cause** based on their symptom
2. **Provide the immediate diagnostic/fix commands** from the reference file's Quick Commands section
3. **Summarize the key resolution steps** from the fetched documentation
4. **If the user is logged into Azure**, offer to run diagnostic CLI commands to confirm the root cause before applying fixes
5. **Recommend next steps** β€” what to verify after the fix, and what to do if it doesn't work

### Phase 5: Escalation (if needed)

If the symptom doesn't match any solution in the reference file, or the fix doesn't resolve the issue:

1. Check Azure Resource Health: `az vm get-instance-view --name <vm> -g <rg> --query "instanceView.statuses" -o table`
2. Offer to restart the VM (requires user approval): `az vm restart --name <vm> -g <rg>`
3. Offer to redeploy the VM (requires user approval β€” moves to new host): `az vm redeploy --name <vm> -g <rg>`
4. Fetch the comprehensive guide: [Troubleshoot RDP connections](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/windows/troubleshoot-rdp-connection) or [Troubleshoot SSH connections](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/troubleshoot-ssh-connection)

---

## Error Handling

| Error                                  | Likely Cause                    | Action                                                                             |
| -------------------------------------- | ------------------------------- | ---------------------------------------------------------------------------------- |
| `fetch_webpage` fails or returns empty | URL may have changed            | Fall back to quick commands in reference file; suggest user check the URL manually |
| CLI command fails with "not found"     | VM name or resource group wrong | Ask user to verify VM name and resource group                                      |
| Run Command times out                  | VM agent not responding         | Route to "VM Agent Not Responding" section in reference file                       |
| Serial Console not available           | Boot diagnostics not enabled    | Run `az vm boot-diagnostics enable` first                                          |
| Password reset fails                   | VMAccess extension error        | Check reference file for VMAccess alternatives (offline reset, Serial Console)     |
| VM stuck in "Updating" after extension op | Extension deadlocked the VM agent | Do NOT add more extensions. Remove stuck extensions via Portal, then restart. See [Pre-Flight Safety Checks](references/cannot-connect-to-vm.md#pre-flight-safety-checks) |
| `VMAgentStatusCommunicationError`      | Agent not reporting status      | Do NOT run extension commands. Use Serial Console or offline repair VM             |

---

## References

- [Cannot Connect to VM β€” Symptom Router](references/cannot-connect-to-vm.md)

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.